C# Unit Test

Unit test is an automated test that verifies if a single unit of a program works as expected. A single unit is the smallest piece of code that can be logically isolated in a program. Usually. it is a single method in a class.

Usually for each project with production code, we create a seperate project with unit tests. We’ll create a new project of class library type. The typical naming convention is that the testing project should be named the same as the project it tests, but it should have the Tests postfix.

We need to install following libraries through NuGet:
– NUnit
– NUnit3TestAdapter: enable us to run tests
– Microsoft.NET.Test.Sdk

We usually create one unit test class for 1 tested class. A collection of unit tests for a specific class is called a test fixture or a test suite. A single unit test is a public void method with a Test attrivute added. We can also add the TestFixture attribute to the class to mark it as a class containing unit tests, but this is not necessary, because if a class contains at least one method with the Test attribute, it will be recognized as a test fixture anyway,

Naming

A name of a unit test shall consist of 3 parts. The name of the method being tested, the exptected behavior and the scenario under which it’s being tested. Don’t hesitate to have a very long test name. It’s better to make it long and clear than short and vague. For example, SumOfEvenNumbers_ShallReturnZero_ForEmptyCollection.

Test Message

A test message is a specific description of the reason for test failure. It is optional, but it may be useful to define it if the name of the test does not carry all the information that we would like to share. Normally for developers they don’t add this message because they can directly jump to the test code to debug.

Assert.AreEqual(10, result, $"For input {inputAsString} the result shall be {expected} but it was {result}.");

AAA Pattern

AAA stands for Arrange, Act, Assert. It describes 3 steps every test should contain.
– Arrage: We prepare all objects we need to run the tested method
– Act: We run the method we want to test
– Assert: We assert that the result is as expected.
Sometimes the arrage and act steps are merged into one. For example: var result = Calculator.Sum(1, 2). Generally, it’s best to keep the act step and the assert step in seperate lines.

Test Case

One of the fundamental rules we should follow when defining unit tests is that they should be thorough. It means we should cover all edge cases. Still, keep in mind that unit tests are rarely 100% thorough, and there can always be some crazy edge cases we didn’t think about. Still, we should make them thorough as we can. TestCase is a way to parameterize the test and provide all kinds of input, and/or the expected result.

[TestCase(1, 2, 3)]
[TestCase(1, -1, 0)]
[TestCase(100, -50, 50)]
[TestCase(0, 0, 0)]
public void Sum_ShallAddNumbersCorrectly(int a, int b, int expected)
{
    var result = Calculator.Sum(a, b);
    Assert.AreEqual(expected, result);
}

There’s a limitation for using TestCase. For TestCase attributes, we can only pass some certain types as attribute arguments: Simple types(int, bool, string etc.) Enums, System.Object, Type, Single-dimensional array of any of them.This may be a serious limitation when defining test cases. To bypass, do this:

[TestCaseSource(nameof(GetSumOfEvenNumbersTestCases))] // we pass a string representing the name of the method that will generate test cases.
public void SumOfEven...(IEnumerable<int> input, int expected)
{...}

private static IEnumerable<object> GetSumOfEvenNumbersTestCases()
{
    return new[]
    {
        new object[] { new int[] {3, 1, 4, 6, 9 }, 10}
        new object[] { new List<int> {100, 200 , 1}, 300}
    }
}

Generally, it is better to use simple TestCase than the TestCaseSource as it is more readable. Still, if we must use types that cannot be used as attribute parameters, we must use TestCaseSource.

Assertion on Exception

[Test]
public void SumOfEvenNumbers_ShallThrowException_ForNullInput()
{
    IEnumerable<int>? input = null;
    
    var exception = Assert.Throws<NullReferenceException>(
        () => input!.SumOfEvenNumbers());
}

Having more than one unrelated assertion in one test is a bad practice (In this case, TestCase is the right choice. Consider the following case:

[Test]
public void SumOfEvenNumbers_ShallReturnZero_IfOnlyNumberInInputIsOdd()
{
    var input1 = new int[] { 1 };
    var result1 = input1.SumOfEvenNumbers();
    Assert.AreEqual(0, result1);
    
    var input2 = new int[] { -7 };
    var result2 = input2.SumOFEvenNumbers();
    Assert.AreEqual(0, result2);
}

We have a serious code duplication here and the test is much longer. Also, we must be aware that the test execution stops at the first failing assertion. That is why it is better to have only 1 assert per test and only have more if they are closely related.

Testing Private Methods

It cannot be tested directly with unit tests because of the access modifier. We should test them indirectly whenuwe test public methods.Remember, private methods are implementation details and such low level thing should not affect our unit tests. Unit tests test the public interface of a class. They verify if it hehaves as it should. We should be able to refactor this class significantly, but as long as the interface of the public methods remains the same, unit tests should not be affected.

Testing Internal Methods

If we decide that for some reason the class should only be accessible within its containing assembly, we should mark it as internal. Remember, this also makes the public method inside it practically internal, because a method can only be used within a context in which it’s containing type can be used. But the problem is, the Unit Tests won’t be working, because the tested code cannot be accessed within other assemblies and our unit tests belong to another project so also to another assembly.

There is a way to bypass this problem. In any file of the project with the code to be tested, we must use a special attribute called InternalsVisibleTo. 举个例子,我们现在的project是Utilities, 然后测试的project是UtilitiesTests,那我们可以在要被测试的那个类所在的cs file里写:

[assembly:InternalsVisibleTo("UtilitiesTests")]

This attribute must be placed before any other code, including the namespace declaration. With this attribute, we declare that internal types and methods declared within this assembly will be accessble in another assembly (which we specify the name of that another assembly in the parenthese). 记住,是整个project(assembly)都可以被UtilitiesTests访问。但是,我们一般不将其写在某个cs文件里,而是写在这个项目的.csproj文件里:

<ItemGroup>
  <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
    <_Parameter1>AnotherProject</_Parameter1>
  </AssemblyAttribute>
</ItemGroup>

这是业界的默认做法。Normally it is NOT recommended using this attribute in any other context than testing internal members until you are 100% sureyou know what you are doing.

Mock

A mock is an object that “pretends” to be another object. It has the same interface as the object being mocked, so it can be used in its place. What makes mocks so useful in unit tests is that we fully control their behavior. Moq is the library we’ll be using for mocking.

[TestFixture]
public class PersonalDataReaderTests
{
    private PersonalDataReader _cut;
    private Mock<IDatabaseConnection> _databaseConnectionMock;

    [SetUp]
    public void Setup()
    {
        _databaseConnectionMock = new Mock<IDatabaseConnection>();
        _cut = new PersonalDataReader(
            _databaseConnectionMock.Object);
    }

    [Test]
    public void Read_ShallProduceResultWithDataOfPersonReadFromTheDatabase()
    {
        _databaseConnectionMock
            .Setup(mock => mock.GetById(5))
            .Returns(new Person(5, "John", "Smith"));

        string result = _cut.Read(5);

        Assert.AreEqual("(Id: 5) John Smith", result);
    }

    [Test]
    public void Save_ShallCallTheWriteMethod_WithCorrectArguments()
    {
        var personToBeSaved = new Person(10, "Jane", "Miller");
        _cut.Save(personToBeSaved);

        _databaseConnectionMock.Verify(
            mock => mock.Write(personToBeSaved.Id, personToBeSaved));
    }
}

Setup method will be executed before each test method.

In NUnit, the order in which tests run is not guaranteed and we shouldn’t make any assumptions about it. It would be better if each test had its own brand-new objects of the tested class and the mocks, and we can do it in a special method with the SetUp attribute. So in the example above, the Setup method will be run before each test.
It is quite a common practice to name the object to be tested as “_cut”: Class Under Test. When using this naming convention, it is very easy to identify the object being tested in this class.

Dependency Inversion & Dependency Injection in Unit Test

Dependency Inversion is one of the SOLID principles. It says that a class should depend on abstractions, not on concrete types.

Dependency Injection is a design pattern in which we give the dependencies to a class via the constructor instead of creating them right in this class.

Dependency Inversion and Dependency Injection work together very well. If we use them, the code is modular and classes are decoupled. They allow easy modification of the implementation details of a class without worrying that we will break other classes. They also allow switching one dependency implementation to another without affecting the code of the class that uses it. A code that does not follow the Dependency Inversion Principle and does not use Dependency Injection is not unit-testable.