Post

Unit Test Guidelines

Writing a good unit test is a major factor in helping maintain a project. Some common mistakes can make debugging a failing test in the future much more difficult than it needs to be. Be kind to your future self and colleagues by considering the following guidelines.

General Guidance

  • Avoid introducing dependencies on infrastructure when writing unit tests. Doing so can make the tests slow, and unreliable, and should be reserved for integration testing.
    Instead, dependencies should be mocked, so that only the Subject Under Test (SUT) is being tested.

  • Tests should also avoid using branching logic. If a test contains if statements, this could be code smell, and it may be more appropriate to have another test or have a Data Driven Test

Naming your tests

The name of your test should consist of three parts:

  • The name of the method being tested.
  • The scenario under which it’s being tested.
  • The expected behavior when the scenario is invoked.

Good test names follow the When_Given_Then notation or similar, see alternatives.
When describes the intent.
Given describes the context.
Then describes the expectation

Naming standards are important because they explicitly express the intent of the test.
Tests are more than just making sure your code works, they also provide documentation. Just by looking at the suite of unit tests, you should be able to infer the behavior of your code without even looking at the code itself. Additionally, when tests fail, you can see exactly which scenarios do not meet your expectations.

❌ Bad test names:

  • Test_Single, Test_Double
  • Test01, Test02, Test03, etc…
  • SomeReallyLongTestNameThatIsNotReadable

Bad test names don’t explain the use case being tested

✅ Good names:

  • Add_SingleNumber_ReturnsSameNumber
  • Subtract_DoubleNumber_ReturnsLowerNumber
  • Multiple_ZeroNumber_ThrowsException

Alternatives

The simple naming conventions above come with their own downsides, for example, if a method name is refactored, you may then also need to update the names of many tests, which isn’t ideal.

Using Behavior-Driven Development (BDD) as an alternative testing methodology, allows developers to use a common vocabulary for writing tests that is shared with stakeholders and QA teams alike. This allows naming tests in a similar way to how a user story is written and helps keep everyone on the same page.

See ChillBDD

Arranging your tests

Arrange, Act, Assert is a common pattern when unit testing. As the name implies, it consists of three main actions:

  • Arrange your objects, creating and setting them up as necessary.
  • Act on an object.
  • Assert that something is as expected.

Why?

  • Clearly separates what is being tested from the arrange and assert steps.
  • Less chance to intermix assertions with “Act” code.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
[TestMethod]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Act
    var actual = stringCalculator.Add("");

    // Assert
    Assert.Equal(0, actual);
}

Data-Driven Tests

Avoid duplicate tests where the only change is the input parameters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[TestClass]
public class MathTests
{
    [TestMethod]
    public void Test_Add1()
    {
        var actual = MathHelper.Add(1, 1);
        var expected = 2;
        Assert.AreEqual(expected, actual);
    }

    [TestMethod]
    public void Test_Add2()
    {
        var actual = MathHelper.Add(21, 4);
        var expected = 25;
        Assert.AreEqual(expected, actual);
    }
}

Instead, consider using a data-driven test where the parameters can be passed in.

1
2
3
4
5
6
7
8
9
10
11
12
13
[TestClass]
public class MathTests
{
    [DataTestMethod]
    [DataRow(1, 1, 2)]
    [DataRow(12, 30, 42)]
    [DataRow(14, 1, 15)]
    public void Test_Add(int a, int b, int expected)
    {
        var actual = MathHelper.Add(a, b);
        Assert.AreEqual(expected, actual);
    }
}

If your data cannot be set into an attribute parameter (non-constant values or complex objects), you can use the [DynamicData] attribute. This attribute allows getting the values of the parameters from a method or a property. The method or the property must return an IEnumerable<object[]>. Each row corresponds to the values of a test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[TestClass]
public class MathTests
{
    [DataTestMethod]
    [DynamicData(nameof(GetData), DynamicDataSourceType.Method)] //Properties also supported
    public void Test_Add_DynamicData_Method(int a, int b, int expected)
    {
        var actual = MathHelper.Add(a, b);
        Assert.AreEqual(expected, actual);
    }

    public static IEnumerable<object[]> GetData()
    {
        yield return new object[] { 1, 1, 2 };
        yield return new object[] { 12, 30, 42 };
        yield return new object[] { 14, 1, 15 };
    }
}

More Info https://www.meziantou.net/mstest-v2-data-tests.htm

This post is licensed under CC BY 4.0 by the author.