
In the practice of unit testing, there is often a desire to make several Assertions in one test method. In theory, this approach is criticized from two main points of view. First, semantically, one test should check only one case, not grow. Secondly, if one of the Assertions in the chain drops, the test will be interrupted and we will see an error message only from it, and it will not come to everyone else that will not give the most complete picture of what happened. The first argument is certainly reasonable and it should always be kept in mind when writing tests, but fanatical adherence to this principle is often not reasonable (see the example below). This post is devoted to the elimination of the second problem. A small class will be presented, allowing you to simply and concisely ensure the execution of several Assertions without interrupting the execution of the method and displaying an error message of each of them.
So, suppose we have a class Size, which, among other things, takes a constructor parameter value in inches, and in itself contains accessors to get the number of whole feet and the remaining inches, that is, passing to input 16, we get 1 foot and 4 inches (12 inches per foot).
public class Size { public int Feet { get; private set; } public int RemainderInches { get; private set; } public Size(int totalInches) {
In order not to spread the tests of the designer on the tree and at the same time provide a suitable coverage I want to write something like:
[Test] public void ConstructorSuccess() { var zeroSize = new Size(0); var inchesOnlySize = new Size(2); var mixedSize = new Size(15); var feetOnlySize = new Size(36); Assert.That(zeroSize.Feet == 0 && zeroSize.RemainderInches == 0, "Zero size"); Assert.That(inchesOnlySize.Feet == 0 && inchesOnlySize.RemainderInches == 2, "Inches-only size"); Assert.That(mixedSize.Feet == 1 && mixedSize.RemainderInches == 3, "Inches and feet size"); Assert.That(feetOnlySize.Feet == 3 && feetOnlySize.RemainderInches == 0, "Feet-only size"); }
Disclaimer: Instead of one truth test, it is possible (and even it would be nice) to use two equality tests each, but in this short example this is not essential, but the code would complicate it.
It is clear that if we select for each such Assertion according to the method, then our test class will very quickly acquire a huge number of methods and in reality, as a result, we will have hundreds of tests, but no more sense. However, in the approach shown, as already mentioned, if one of the Assertions of the data from the rest falls, we will not see, because method execution will stop.
')
Let's start to eliminate this inconvenience.
In NUnit, the test crashes when any Exception that is not caught occurs, and the Assert class fails AssertionException with full error messages. Thus, in essence, we need to ensure that exceptions are captured throughout the test method, the accumulation of their messages and the output of the accumulated at the end. Naturally, it is a terrible horror to do this explicitly, right in the code of the test itself.
After some reflection, for this purpose a battery class was proposed, the use of which inside the test method from the example above is as follows:
var assertsAccumulator = new AssertsAccumulator(); assertsAccumulator.Accumulate( () => Assert.That(zeroSize.Feet == 0 && zeroSize.RemainderInches == 0, "Zero size")); assertsAccumulator.Accumulate( () => Assert.That(inchesOnlySize.Feet == 0 && inchesOnlySize.RemainderInches == 2, "Inches-only size")); assertsAccumulator.Accumulate( () => Assert.That(mixedSize.Feet == 1 && mixedSize.RemainderInches == 3, "Inches and feet size")); assertsAccumulator.Accumulate( () => Assert.That(feetOnlySize.Feet == 3 && feetOnlySize.RemainderInches == 0, "Feet-only size")); assertsAccumulator.Release();
Another example of use (I hope the code speaks for itself and is clear without comments):
Result<User> signInResult = authService.SignIn(TestUsername, TestPassword); var assertsAccumulator = new AssertsAccumulator(); assertsAccumulator.Accumulate(() => Assert.That(signInResult.IsSuccess)); assertsAccumulator.Accumulate(() => Assert.That(signInResult.Value, Is.Not.Null)); assertsAccumulator.Accumulate(() => Assert.That(signInResult.Value.Username, Is.EqualTo(TestUsername))); assertsAccumulator.Accumulate(() => Assert.That(signInResult.Value.Password, Is.EqualTo(HashedTestPassword))); assertsAccumulator.Release();
The result of the execution of this example with the output of two errors is simultaneously shown on the enticing screen at the beginning of the post.
The AssertsAccumulator implementation looks like this:
public class AssertsAccumulator { private StringBuilder Errors { get; set; } private bool AssertsPassed { get; set; } private String AccumulatedErrorMessage { get { return Errors.ToString(); } } public AssertsAccumulator() { Errors = new StringBuilder(); AssertsPassed = true; } private void RegisterError(string exceptionMessage) { AssertsPassed = false; Errors.AppendLine(exceptionMessage); } public void Accumulate(Action assert) { try { assert.Invoke(); } catch (Exception exception) { RegisterError(exception.Message); } } public void Release() { if (!AssertsPassed) { throw new AssertionException(AccumulatedErrorMessage); } } }
As you can see, only two methods are exposed, Accumulate () and Release (), the use of which is quite transparent. Receiving a delegate using the Accumulate method makes the class very versatile, you can pass any kind of Assertion (as shown in the signInResult example) and if necessary, you can easily adapt the class to any other test framework by changing only the type of Exception being thrown inside Release ().
From the examples it is clear that the class allows you to conveniently write test methods that contain several Assertions, while always running to the end and having a complete output of information about errors.
In conclusion, I would like to remind you that fanatical adherence to any principle is rarely a good thing, and excessive use of such a class is no exception. It should be understood that it can be used only when several Assertions actually test one semantic isolated operation or script and placing them in one test is justified.