Skip to content

Unit Test and Integration Test Guidelines.md

jacksonbalfour edited this page Mar 20, 2022 · 1 revision

Unit Testing

All additions and modifications of the Horizon Simulation Framework should have an associated unit test. Continue reading to find out why and how

How to write a unit test

First create a unit test class in a dedicated unit testing folder at the same level as the other unit testing folders.

This class should have the following header

using NUnit.Framework;
using System;
using System.Collections.Generic;`

namespace assemblyName_UnitTest
{
    [TestFixture]
    public class className_UnitTest
    {
        /// <summary>
        ///
        /// </summary>
        [Test] // this is a tag, if using [Test], the method will be recognised as a test to be run, 
               // if you'd like to use a 'helper' function and require it to run before all other tests, use the [SetUp] tag
        public void ClassAndMethodName_UnitTest()
        {
         //arrange
         //act
         //assert
        }
    }
}

Inside the method ClassAndMethodName_UnitTest() is where your unit test goes.

Best Practices

Unit testing aims to encourage better code design and reduce development timelines by reducing time spent functional testing, fixing regression defects, debugging, and rolling out. However, poorly designed tests can seriously hinder development. Best practices should be followed to avoid common mistakes which lead to ineffective testing.

Arrangement

Consistent code arrangement is an effective organizational tool which increases readability of test code. Tests are commonly conducted in three steps: arrange, act, and assert. The 'arrange' step gathers the necessary structures and objects to perform the action. The 'act' step is the call to the method or constructor that is being tested. After the action, are the 'assert' step compares the resulting objects to expected values or objects, using assert statements. Separating these sections with comment headers helps communicate the purpose and structure of a test at a glance, and prevents mixing “actions” with assertions, which hinders readability. Use the existing unit tests as a reference.

Fast

Larger projects may have thousands of unit tests. In the best-case scenarios, slow tests can be an annoyance to developers who must wait during long test runs. In worst case scenarios, it can clog a CD/CI pipeline which can’t complete test runs in between pull requests. Therefore, it is expected that each unit test take as little time as possible to run, on the order of milliseconds.

Self-checking and Repeatable

Unit tests should be able to automatically detect if it has passed or failed, that is, without human interaction \cite{bestpractice}. The assert function is included in all test frameworks for this purpose. Test runs should also provide consistent results between runs if nothing is changed. In unit testing, the use of objects or methods which may change their outputs over time should be avoided (e.g. Current time, random generators, non-local databases).

Uncoupling Code

Unit testing alongside development encourages less coupled code. Because highly coupled code is, by its definition, hard to isolate, testing involves constructing many objects and methods which are not the subject of the test. This is bad practice because it makes locating the cause of failure more difficult and makes tests run longer than necessary. A developer can notice that, in the arrangement section of a test involving highly coupled code, there are many objects and method calls which don’t provide relevant information to the function or object being tested. This feedback naturally decouples code because it would be more difficult to test otherwise, resulting in simpler, more easily understood, and more easily tested code

Naming

Organization is instrumental in delivering the benefits of testing. Tests are only useful if they are easily understood and assist in rooting out bugs. Test naming is an often overlooked but instrumental organizational tool. Tests should have specific names which indicate which method or object is used and the specific aspect being tested. So rather than Object_Test1, a developer should use a more specific name like Object_function_ErrorExpected or Object_Ctor_typeOverload. Good names describe the behavior of each test, eliminating the need to dissect the code itself in the case of a failing test \cite{bestpractice}.

Avoid Logic

Logical statements increase the likelihood of an error and make code less readable. Logic statements like if, while, for, foreach, switch, etc. should be avoided. If tests are complicated to understand or were difficult to write a passing test because of tricky logic, a developer is less likely to trust the tests. Developers should take test results seriously and have confidence that a failed test is the result of faulty code. Tests which aren’t trusted are useless to developers \cite{bestpractice}.

Test Private Methods by Calling Public Methods

Private methods represent a challenge for developers writing unit tests. How should one invoke the method in the ‘act’ part of a unit test? The answer is simple: call them the way the program uses them, through a public method. Private methods never exist in isolation. There is always a way to call a private method from a public method. Since the program will always call the private method this way, it is useful to consider the private method as a part of the same unit as the public method