As you expand your knowledge of testing, be sure to note best practices and keep them in mind as you code. For example, at this point, you should know to always start your test case with a descriptive and meaningful name, and you should cover only one unit of code at a time.
In addition, don't forget the F.I.R.S.T. principles of good unit testing: a well-written test is Fast, Isolated, Repeatable, Self-validating, and Timely!
In order to continue building your repertoire of best practices, you'll need to understand the lifecycle of your unit tests, which will be the focus of this chapter.
Learn the unit test lifecycle
By understanding the lifecycle of your unit tests, you can code around the main lifecycle routines in your code.
Instead of making declarations as part of your test methods, you can test whole components with subsequent actions running as part of the same test suite and execute specific routines when test cases are executed at the beginning or the end of the execution.
For example, you can declare private members or properties in your test suite and have test methods to run over the same instance. For these cases, knowing the lifecycle of a unit test becomes useful to the test runner when executing as part of the xUnit after it is set up or finalized.
To set up the test, use a constructor. (In other frameworks, thesetUp
method is used).
To finalize the test, use the IDisposable interface. This is called disposal. (In other frameworks, the tearDown
method is used).
Let's introduce a new feature to our CalculatorProgram and see how we can take advantage of the unit test lifecycle with xUnit.
Explore the lifecycle by adding a new feature
Let's say our CalculatorProgram was released and introduced to users, and they like it. In their feedback, users expressed that they would like the program to have an "in-memory" support that helps to keep a sequence of sums in the same session, pretty much like other calculators.
For example, if a user makes a first sum and then wants to continue with the same operation by adding another number, the previous value needs to be stored in memory for the incoming number to be applied (and then subsequently stored).
So if we have passed 1, then 2, through the addition operation, the calculator should store the sum (in this case, a value of 3) in memory, so that if we send another number, like 7, the calculator will be able to use the stored sum and deliver a new value of 10. And, of course, the user will also need to be able to clear the InMemory value by telling the CalculatorProgram to "reset" its value back to zero.
To support this new feature, we can add a property to our CalculatorProgram for storing a sum and then add a new Add
method:
using System;
public decimal Sum(params decimal[] numbers)
{
if (numbers.Length == 1)
{
Current += numbers[0];
return Current;
}
var result = 0.0M;
for (int i = 0; i < numbers.Length; i++)
{
result += numbers[i];
}
return result;
}
Initialize an xUnit test with the constructor
In xUnit, a test setup is typically handled by the constructor, which provides a natural way of instantiating things following the object-oriented paradigm.
Then, the test runner of an xUnit program starts by instantiating your test class and giving you control to set up whatever you need, typically the instance of the class that you want to cover under unit testing.
Finalize an xUnit test with disposal
On the other hand, when you need a particular sentence or group of lines in C# to be executed after a test gets executed, you can implement the IDisposable interface in the declaration of your test class.
Remember in order to implement the IDisposable interface, you must declare and provide the body for the the Dispose
method and then execute the code that you need, if any, as well as final assertions as required.
using System;
using CalculatorProgram;
using Xunit;
namespace UnitTests
{
public class CalculatorMemoryTests : IDisposable
{
Calculator Calculator { get; }
public CalculatorMemoryTests()
{
Calculator = new Calculator();
}
[Fact]
public void AddNumberTest()
{
Calculator.Reset();
// Act
Calculator.Sum(3);
Calculator.Sum(7);
Assert.Equal(10, Calculator.Current);
}
}
}
Let's recap!
When your tests need to handle several states of multiple usages over the same component, you can use private members or properties in your test classes and have more control over the lifecycle, or the initialization and disposal of your unit tests. This can be useful when you need to set up some specific declarations with a constructor or have a specific set of C# sentences that are required after the unit tests get executed.