We all probably know about the importance of good unit test suite for our projects and about the Arrange-Act-Assert pattern we should be using in our tests. I think I'm lot more pragmatist than purist and I don't have anything against the tests not following the AAA form as long as I can read, understand and maintain them easily. But there's one thing I do consider a must in each and every unit test - an assertion.
Assertion not only decides whether the test passes or fails, it also makes the purpose of the test clear and ensures it is verifiable. Test without assertion is nothing more than a random snippet of code.
What I consider to be the assertion?
- A direct call to a known asserting method, provided most often by the testing frameworks, assert or mocking libraries, etc., like the classic Assert.Equal(actual, expected) or Shouldy's extension method like actual.ShouldBe(expected). The naming convention of these constructs always makes it clear that they actually verify the outcome - "assert that...", "verify that...", "something should be...".
- An indirect call to a method described above. When verification of the outcome is not just a comparison of a single object (or the comparison we need is not included in our favourite testing framework), we often create our own helper methods that play a role of a single logical assertion, despite multiple physical ones, i.e.
private void ShouldBeAValidExternalUri(this string value) { value.StartsWith("http://our.domain.com/").ShouldBeFalse(); Uri.IsWellFormedUriString(value, UriKind.Absolute).ShouldBeTrue(); }
That's still a great assertion, call to ShouldBeAValidExternalUri will eventually trigger a call to the known asserting method provided by the framework. It's nice if we maintain the naming schema that indicates we're going to verify something in our helper method. - Throwing an exception within the test or within the helper asserting method. It's technically the equivalent of calling Assert.Fail. It also provides a good way of describing what went wrong using the exception message as it will probably end up shown in the test runner.
What I do not consider to be the assertion?
Throwing an exception in the SUT itself! It is quite an often thing to see in some projects - tests that call a method on the tested object with an assumption that the tested method will throw if something goes wrong.
public class Sut { public static void DoTheWork(int howManyTimes) { if (howManyTimes <= 0) throw new ArgumentOutOfRangeException("howManyTimes"); for (var i = 0; i < howManyTimes; ++i) DoTheRealWork(); } // ... } public class Test { [Test] [ExpectedException(typeof(ArgumentOutOfRangeException))] public void SutThrowsWhenNegativeCounterPassedIn() { Sut.DoTheWork(-1); } [Test] public void SutDoesntThrowWhenPositiveCounterPassedIn() { Sut.DoTheWork(1); } }
The Sut class is a perfectly valid piece of defensive code. And we have two tests, one checking if the exception is thrown when negative number is passed, and the second...? What is the second testing? Technically it works - if an exception is thrown in the tested code, the test would fail. If not - it would pass, proving that no exception was thrown.
But what is the value given by this test? It is not verifying whether the work was actually done. What more, according to the TDD rule that we should have only the minimal amount of code making the test pass, the correct implementation of the DoTheWork method required by SutDoesntThrowWhenPositiveCounterPassedIn test is a no-op! Both tests will still pass if we remove the whole loop. So was there anything really tested?
A good test should always make it clear and explicit what is tested and what is the expected outcome. "Expect no exception" approach doesn't follow that rules. "No exception" is not a good outcome if an empty method is enough for it to pass. And proving that your code doesn't do something is much more difficult than proving what it does.
Of course, as always, there are some cases when it's maybe acceptable to have tests without explicit assertion. Tests for validation methods that exist only to throw when certain condition is not met may be a good example. Another example is smoke testing, as a part of integration test suite. When we have the functionality well-covered with lower-level unit test suite and we just want to ensure that the code doesn't blow up when run together and verifying the outcome at this level is hard enough, we may go with "expect no exception" test.
But even in those cases I personally would use some kind of explicit construct to show the intent of the test. For example, NUnit offers assertion like this:
Assert.DoesNoThrow(() => Validator.Validate("valid string"));
It's not perfect, but at least more explicit than the sole SUT method call.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.