Open Crossbow78 opened 4 years ago
Some of what you point out is by design, some things may be bugs and some things are just the way they are because... "History." Also, since fluent syntax goodness is a bit subjective, we may reasonably disagree about what is good... :smile:
Assert.That(x.Is.equalto(y))
. I doubt that will make it into NUNit but it may be in the next framework I work on.On this one, we will probably just have to agree to disagree. I would find Assert.That(anArray, Has.Count.EqualTo(5))
quite surprising if it worked. Arrays as we know don't have counts. NUnit was created with programmers in mind, of course.
Has.Exactly
was invented to solve this problem and solves it well. If there were a new major release of NUnit. I would favor removing both Length
and Count
, which are simply syntactic sugar over Has.Property("Length")
etc. BTW, that should explain why we compile and don't fail until runtime... we are testing whether an object has a property with a certain name.
I think you may have found a bug - or several - here. NUnit compiles but errors when you apply a constraint to a scalar value, which only works on vectors. (See below for why this has to be a runtime error.) However, nothing similar happens when an operation requires a scalar value but a vector is supplied. So Assert.That(myArray, Is.Not.Zero)
and the equivalent Assert.That(myArray, Is.Not.EqualTo(0)
are valid and pass because myArray is most definitely not equal to zero!
The heading is actually the answer to many "why does NUnit..." questions. NUnit rarely treats values as typed. At the time it was created, it was not possible to use generics, so all examination of the type of arguments and conversion between types was handled under the covers by NUnit itself. If you look at asserts, you will see that there are some generic methods, but the underlying implementation as objects remains.
IMO this is the biggest weakness of the current implementation. Several attempts have been made to convert the entire Assert class to use generics and to test for compatibility between the actual values and the constraints at compile time. I made two attempts myself. So far, it has not been possible to do this in a backward compatible way. Backward compatibility, of course, is a requirement for any change to NUnit barring a major release (i.e. NUnit 4). So I suggest continuing to press for improvements you want in that context.
Taking All.All.All
as an example, I think that should fail. We have a set of tests that attempt to compile things that should not compile and that ought to be added to them. I guess that over the years things got added without adding to the compilation tests.
BTW, it may not be obvious, but the addition of And
and Or
to the NUnit syntax is the cause of a lot of difficulty in implementation. It's a good order of magnitude easier to implement a fluent syntax that is entirely linear... adding those operators more than tripled the amount of code needed to implement what we have. If I were starting from scratch, I might not include them as operators.
For things in the above that are bugs, I urge you to submit them to the nunit team. For those changes that can't be done until NUnit 4, I recommend showing up to lobby for the stuff you want.
Fair warning: I don't work much on NUnit any longer. I answered here because the NUnit fluent syntax was originally my design but others will need to decide where NUnit is going next.
Thanks for your elaborate response, and for bringing structure into my questions.
I agree that separating the condition from the action has a beautiful elegance, and I do miss something like the Assume
in FluentAssertions. I hadn't even read about Warn
, thanks for bringing it to my attention.
About the counts/lengths, I can totally understand this comes from history and backward compatibility. It could be an idea to mark these as obsolete to at least guide the developer to a better alternative. I call it better, since the intent clearly is to validate how many items a 'collection' contains, without caring about details like the type of collection. I found myself refactoring some return type from List<>
to IEnumerable<>
and everything compiled fine, but tests suddenly failed because the 'Count' property no longer existed.
So are you saying a syntax like the following is impossible, simply due to the fact that internally everything is type-less?
Assert.That(myArray, Contains.Single(x => x.IsActive).With.Name.EqualTo("Test"))
I can imagine it's not a small task to modify that in existing code, but given the powerful possibilities it would deliver, it could be interesting to explore. Is one of these earlier attempts preserved in a branch somewhere?
I'll submit the syntax bugs as a separate issue.
I wouldn't say that such an expression is impossible, but I do think it requires a fairly complete rewrite of the underlying code that (sort of) parses the syntax as the expression is evaluated. Personally, I think there is a need for two approaches coexisting. One typed approach such as we are discussing and a legacy object
approach for dealing with the worst case scenario - collections of mixed types. In fact, such collections are pretty rare these days anyway, so maybe we can make that usage fade away.
My own attempts are gone. @jnm2 didn't you work on this once? Do you have any lessons learned for @Crossbow78 ?
Yes, I tried once or twice and ran into backwards compatibility issues. The biggest learning from my own experiences outside NUnit is that fluent APIs are a lot of work, and fluent APIs that are context-dependent are a lot of work squared (so to speak).
We can augment the compiler type checks with our own analyzer errors and warnings. The upside there is that there are no backwards compatibility concerns (the analyzer is very new and isn't at 1.0 yet). The downside is that it's probably even more maintenance work to do this checking in the analyzer.
I've been using Shouldly along with NUnit for a while. It's my current favorite for assertion statements. NUnit is my favorite for test case organization, custom attributes, and the "batteries included" selection of built-in attributes that cover common scenarios.
If anyone is interested, I'm currently rewriting the whole constraint-building thing for my TCLite framework. If I can get it to work, it might be useful here.
@CharliePoole I'm generally interested. A month ago I tried again to retrofit true-async assertions and I have some thoughts on that too. Is that something you're including?
I think that's an important issue but it's orthogonal to the question of generics. So far, I've only implemented just enough assertions and constraints to exercise the builder because otherwise each little change I make ramifies into too many files.
Coming from XUnit + FluentAssertions, I must say I love the TestCase feature of NUnit.
At the same time I feel that the assertion syntax seems all over the place. The fluent attempt mostly applies to reading the assertion, but definitely not to writing them, because Intellisense gives many false suggestions. And for the number of constraints listed in documentation the possibilities still seem rather limited.
There's nothing preventing me from using FluentAssertions, but I'm trying to understand the philosophy in the current assertion/constraint design in NUnit, especially since a lot of work seems to have gone into it.
First example of clunkiness: why does the
Has
class have a 'Length' and 'Count' which only work on classes with that exact property name? This leads to tests seemingly perfect by reading the code, but still failing at runtime because a collection was of an unexpected type. It's just confusing while there is a syntax that always works...Other examples of clunkiness when it comes to Intellisense:
Finally, I'm wondering whether it's possible to (1) match using a lambda expression, and (2) drill down into a match to assert its properties. Is there something equivalent in NUnit?