Unit Testing – Guidelines

All of us agree that testing is good and there are numerous advantages to writing unit tests. However, sometimes we have to disagree on how to do our testes, TDD or BDD, what should be our code coverage goal and how can we do it?

The propose of unit tests is to validate that each unit of code performs as expected. It’s the first line of tests or the first line of defense to developers. They are implemented and performed by software developers since the earlier stages of the developer process and for several tools during all the process.

 

Advantages of unit testing:
  1. Lesser bugs are deployed, the product delivered to the client has better quality. Happy clients and the increased of reliance on customer service, quality assurance teams, and bug reports.
  2.  Unit testing ensures that we don’t break anything when a refactoring is needed because we can always find opportunities to improve our code and sometimes we really have to do.
  3. You don’t have to tests everything manually every time you make a change, or you add a new feature.
  4.  The developer has a way to verify the behavior of their code between edits rapidly. The feedback is much faster than with functional and integration tests.
Rules of thumb:

All the tests should respect a list of principles:

  1. Unit tests ensure that individual components work appropriately in isolation from the rest of the code. A unit tests should focus on a single ‘unit of code’;
  2. Unit tests must be isolated from dependencies, no network access, and no database requests.
  3. The tests should provide a clear description of the feature being tested. It should be provided on the test name with the following template:
    MethodUnderTest_inputOrScenarioUnderTest_expectedResult
    Bad example:

    [TestMethod]
    public void UnitTest_ClosetEnemy()
    {…}

    Good example:

    [TestMethod]
    public void ClosestEnemy_Matrix2X2WithoutTwoElement_ReturnZero()
    {
    // Arrange
    const int EXPECTED_NEGATIVE_VALUE = 0;
    var positions = new int[][] { new[] { 1, 0 }, new[] { 0, 0 } };

    // Act
    var actualValue = ClosestEnemy(positions);

    // Assert
    Assert.AreEqual(expected: EXPECTED_NEGATIVE_VALUE,actual: actualValue, message: $”ClosestEnemy expected  value when there isn’t one element was {EXPECTED_NEGATIVE_VALUE} and the actual value is {actualValue}”);
    }

  4. The tests should be arranged with the common pattern Arrange, Act, Assert.
    • Arrange: creating objects and setting them up as necessary;
    • Act: act on an object;
    • Assert: assert what is expected;

    Separating all of this actions the test highlights the dependencies and what the test is trying to assert. The principal advantage is the readability.

    Bad example:

    [TestMethod]
    public void ClosestEnemy_Matrix2X2WithoutTwoElement_ReturnZero()
    {
    var positions = new int[][] { new[] { 1, 0 }, new[] { 0, 0 } };

    Assert.AreEqual(expected: 0, actual: ClosestEnemy(positions), message: $”ClosestEnemy expected value when there isn’t one element was 0 and the actual value is {ClosestEnemy(positions)}”);
    }

    Good example:

    [TestMethod]
    public void ClosestEnemy_Matrix2X2WithoutTwoElement_ReturnZero()
    {
    // Arrange
    const int EXPECTED_NEGATIVE_VALUE = 0;
    var positions = new int[][] { new[] { 1, 0 }, new[] { 0, 0 } };

    // Act
    var actualValue = ClosestEnemy(positions);

    // Assert
    Assert.AreEqual(expected: EXPECTED_NEGATIVE_VALUE, actual: actualValue, message: $”ClosestEnemy expected value when there isn’t one element was {EXPECTED_NEGATIVE_VALUE} and the actual value is {actualValue}”);
    }

  5. The input should be the mildest possible and just the necessary to tests the current scenario. When creating a new test we want to focus on the behaviors and avoid unnecessary information that can introduce errors in our tests and turn the test hard to read.
  6.  The test should be part of the delivery pipeline and on failure should provide an explicit message or report with the information about what were you testing,  what should it do, what was the output and what was the expected result.
  7.  Avoid undeclared variables. An undefined variable can confuse the reader of the test and can cause errors.Bad example:
    // Act
    var actualValue = GetCityName();

    // Assert
    Assert.AreEqual(expected: “cityName”, actual: actualValue, message: $”CityName expectded value was cityName and the actual value is {actualValue}”);

    Good example:

    // Arrange
    const string EXPECTED_NAME = “Porto”;

    // Act
    var actualValue = GetCityName();

    // Assert
    Assert.AreEqual(expected: EXPECTED_NAME, actual: actualValue, message: $”CityName expectded value was {EXPECTED_CITY_NAME} and the actual value is {actualValue}”);

  8. Avoid multiple asserts in the same test. With multiple asserts per test, if one asset fails the subsequent asserts will not be evaluated and we cannot figure out what is really failing on a single view.
  9. Don’t generate random data. Usually, random data is irrelevant to the test propose. If the data is irrelevant why should we generate it? It just make our code mode unreadble and if the test fail we don’t know the value generated and why it failed unless we log it.

Bad example:

// Arrange
const string ANY_NAME =RandomStringUtils.random(8);

// Act
var actualValue = GetCityName();

// Assert
Assert.AreEqual(expected: ANY_NAME , actual: actualValue, message: $”CityName expectded value was {ANY_NAME } and the actual value is {actualValue}”);

Good Example:

// Arrange
const string EXPECTED_NAME = “Porto”;

// Act
var actualValue = GetCityName();

// Assert
Assert.AreEqual(expected: EXPECTED_NAME, actual: actualValue, message: $”CityName expectded value was {EXPECTED_CITY_NAME} and the actual value is {actualValue}”);

Mocks

Mocking is necessary for unit testing when the unit that being tested has external dependencies. The goal is to replace the behavior or state of the external dependency.
Most languages now have frameworks that make it easy to create mock objects. That’s why using interfaces for your dependency make it ideal for testing. The mocking framework can easily make a mock of an interface simulating the behavior of the real ones.
We can also implement Fakes with the Intuit to replace the dependencies behavior. Fakes replace the original code by implementing the same interface. The disadvantage is that fakes are hard to code to return different results in order to test several user cases and tests can become difficult to understand and maintain with a lot of “elses”.

Example:

Supposing we have the class PizzaBuiller, and this class has one dependency, the CheeseBuilder. We need to have an interface ICheeseBuilder and resolve this dependency with dependency injection.

Our class will be:

public class PizzaBuilder
{private readonly IBreadBuilder breadBuilder;public PizzaBuilder(IBreadBuilder breadBuilder)
{
this.breadBuilder = breadBuilder;
}public Pizza GetPizza()
…. }

Using interfaces we can easily mock their methods and manipulate just the data needed to our unit tests:

var mockedBread = “Bread”;
var mockBreadBuilder = new Mock<IBreadBuilder>();
mockBreadBuilder.Setup(x => x.GetBread(It.IsAny<int>())).Returns(mockedBread);
TestInitialize

The method with the tag [TestInicialize] will be executed before every test. It’s the perfect place to define and allocate resources needed by all the tests. But be careful using this tag, because this is not a bag to put all the arranges, sometimes is more clear what is the test propose with all the arranges inside the test.

Example:

[TestInitialize]
public void Initialize()
{
var mockedBread = “Bread”;
var mockBreadBuilder = new Mock<IBreadBuilder>();
mockBreadBuilder.Setup(x => x.GetBread(It.IsAny<int>())).Returns(mockedBread);

}

Code coverage

Code coverage regards to the quantity of code covered by test cases. Usually, developers try to produce a high level of coverage, but 100 % of code coverage doesn’t mean that we know with 100% assurance that the code does what it should do. Because there are two kinds of coverage:

  • Code coverage: how much of the code is executed;
  • Case coverage: how many of the use-cases are included by the test

Case coverage refers to how the code will behave in different real-world scenarios, and it can depend on several situations making impossible cover all the cases. 100% code coverage does not ensure 100% case coverage.

Should I do unit tests for everything?
If your code doesn’t have any logic, you can have 0% of code coverage or exclude from the code coverage. If your code relies on I/0 dependencies like query a database, capture user input, and so on it will not be easy to mock and test, If you have to do a bunch of mocking to create a decent unit test, perhaps that code doesn’t need unit tests at all.

TDD – Test-Driven Development

TDD focuses on create tests to test how the code is implemented. TDD drives developers to focus on product requirements before write code, contrary to the standard programming where developers write unit tests after the developing the code. Following it, no logic in code is written without unit tests what makes possible to have a very hight test-coverage

The process is always:

  • Write one test;
  • Watch it fail;
  • Implement the code;
  • Watch the test pass;
  • Repeat;

Step by Step process:

To explain the TDD process, I will use a challenge named Close Enemy. The goal is from a  matrix of numbers which will be a 2D matrix that includes just the numbers 1, 0, and 2. Then from the position in the matrix where the element “1” is, return the number of spaces either left, right, down, or up we have to move to reach an enemy that is represented by a 2. In a second phase, we should be able to wrap around one side of the matrix to the other as well. For example: if our matrix  is [“0000”, “1000”, “0002”, “0002”] then this looks like the following:

0 0 0 0
1 0 0 0
0 0 0 2
0 0 0 2

For this input our function should return 2 because the closest enemy  is 2 spaces away from the 1 by moving left to wrap to the other side and then moving down. The array will contain any number of 0’s and 2’s, but only a single 1. If we cannot find any 1 the function should return -1. It may not contain any 2’s at all as well, where in that case should return a 0.

  1. Decide the inputs and outputs and the function signature. Our input will be an int matrix and the output the space between the one element and the closest enemy.
    public int ClosestEnemy(int[][] positions)
    {
    retrurn 0;
    }
  2. Implement one test to fail. To start, we should test the basics that my function should do, the first one or two lines. TDD is about the focus on tiny things only. So, first validate if the one element exists in the matrix. If it doesn’t exist the function should return -1. The test will fail because at this point we still not have the node needed to the feature.
    public void ClosestEnemy_Matrix2X2WithoutOneElement_ReturnNegativeOne()
    {
    // Arrange
    const int EXPECTED_NEGATIVE_VALUE = -1;
    var positions = new int[][] {new [] { 0, 0 }, new []{ 0, 2 } };

    // Act
    var actualValue = ClosestEnemy(positions);

    // Assert
    Assert.AreEqual(expected: EXPECTED_NEGATIVE_VALUE, actual: actualValue, message: $”ClosestEnemy expectded value when there isn’t one element was {EXPECTED_NEGATIVE_VALUE} and the actual value is {actualValue}”);
    }

  3. Implement the code to fix the test:
    public int ClosestEnemy(int[][] positions)
    {
    var onePosition = this.GetOnePosition(positions);
    if (onePosition == null){
    return -1;
    }
    return 0;
    }
  4. Implement another test. Given a matrix without any two, the funtion should return 0. Watch the test failing.
    [TestMethod]
    public void ClosestEnemy_Matrix2X2WithoutTwoElement_ReturnZero()
    {
    // Arrange
    const int EXPECTED_NEGATIVE_VALUE = 0;
    var positions = new int[][] { new[] { 1, 0 }, new[] { 0, 0 } };

    // Act
    var actualValue = ClosestEnemy(positions);

    // Assert
    Assert.AreEqual(expected: EXPECTED_NEGATIVE_VALUE, actual: actualValue, message: $”ClosestEnemy expectded value when there isn’t one element was {EXPECTED_NEGATIVE_VALUE} and the actual value is {actualValue}”);
    }

  5. Fix the code and watch the test passing.
    public static int ClosestEnemy(int[][] positions){
    var onePosition = GetOnePosition(positions);
    if (onePosition == null)
    {
    return -1;
    }
    var listOfEnemies = GetAllTheEnemies(positions);if (!listOfEnemies.Any())
    {
    return 0;
    }
    return 1;
    }
  6. Implement another test. Given a valid matrix, the function should return the minimum space between the one element and the two element. Watch the test failing:
    [TestMethod]
    public void ClosestEnemy_Matrix2X2_ReturnMinimuValueSpace()
    {
    // Arrange
    const int EXPECTED_EMPTY_SPACE_VALUE = 2;
    var positions = new int[][] { new[] { 1, 0 }, new[] { 0, 2 } };
    // Act
    var actualValue = ClosestEnemy(positions);
    // Assert
    Assert.AreEqual(expected: EXPECTED_EMPTY_SPACE_VALUE, actual: actualValue, message: $”ClosestEnemy expectded value was {EXPECTED_EMPTY_SPACE_VALUE} and the actual value is {actualValue}”);
    }
  7. Developer the code to fix the test:
    public int ClosestEnemy(int[][] positions)
    {
    var onePosition = this.GetOnePosition(positions);
    if (onePosition == null)
    {
    return -1;
    }
    var listOfEnemies = this.GetAllTheEnemies(positions);
    if (!listOfEnemies.Any())
    {
    return 0;
    }var listOfSpaces = listOfEnemies.Select(enemy => CalculeSpaces(onePosition, enemy));
    return listOfSpaces.Min();
    }

    private int CalculeSpaces((int, int)? onePosition, (int, int) enemy)
    {
    var Xspaces = Math.Abs(onePosition.Value.Item1 – enemy.Item1);
    var Yspaces = Math.Abs(onePosition.Value.Item2 – enemy.Item2);
    return Xspaces + Yspaces;
    }

  8. Implement a test to guarantee that we can wrap around one side of the matrix to the other as well. Watch the test failing.
    [TestMethod]
    public void ClosestEnemy_Matrix4x4_ReturnAroudEmptySpace()
    {
    // Arrange
    const int EXPECTED_EMPTY_SPACE_VALUE = 2;
    var positions = new int[][] { new[] { 0, 0, 0, 0, }, new[] { 1, 0, 0, 0 }, new[] { 0, 0, 0, 2 }, new[] { 0, 0, 0, 2 } };

    // Act
    var actualValue = ClosestEnemy(positions);

    // Assert
    Assert.AreEqual(expected: EXPECTED_EMPTY_SPACE_VALUE, actual: actualValue, message: $”ClosestEnemy expectded value was 0 and the actual value is {actualValue}”);
    }

  9. Implement the code to fix the test.
    public static int ClosestEnemy(int[][] positions)
    {
    var onePosition = GetOnePosition(positions);
    if (onePosition == null)
    {
    return -1;
    }
    var listOfEnemies = GetAllTheEnemies(positions);
    if (!listOfEnemies.Any())
    {
    return 0;
    }
    var listOfSpaces = listOfEnemies.Select(enemy => CalculateAllTheSpaces(onePosition, enemy, positions.Length));
    return listOfSpaces.Min();
    }private static int CalculateAllTheSpaces((int, int)? onePosition, (int, int) enemy, int matrixSize)
    {
    var Xspaces = Math.Abs(onePosition.Value.Item1 – enemy.Item1);
    var Yspaces = Math.Abs(onePosition.Value.Item2 – enemy.Item2);
    var space = ShorterPath(Xspaces, matrixSize) + ShorterPath(Yspaces, matrixSize);
    return space;
    }private static int ShorterPath(int spaces, int matrixSize)
    {
    return spaces <= matrixSize – spaces ? spaces : matrixSize – spaces;
    }


BDD – Behavior Driven Development

BDD focus to test the behavior that is related to business outcomes. Instead of thinking just how the code is implemented we spend some time thinking about how the scenario is. The language used to define the unit tests should be more generic and understandable by all the intervenients in the project, including stakeholders for example.

BDD and TDD are not enemies. In true BDD could extend the process of TDD with better guidelines.

Conclusion:

Yes, you have to spend more time to implement TDD in your project, but I ensure that your client will find much fewer bugs and you can easily refactor your code without any fear. It’s all about priorities, the cost of a bug that makes it into production could be many times larger than the cost of the time spent implementing unit tests.

References:

https://medium.com/javascript-scene/5-common-misconceptions-about-tdd-unit-tests-863d5beb3ce9

https://www.computer.org/csdl/mags/so/2007/03/s3024.pdf

https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d

https://www.sitepoint.com/javascript-testing-unit-functional-integration/

https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices

Leave a Reply

Your email address will not be published. Required fields are marked *