Apply the 3A steps of unit testing
The 3A (or AAA) pattern is a common way of writing unit tests. 3A stands for arrange, act, assert, which are the three functional sections that make up your test:
Arrange all necessary preconditions and inputs for the object or method under test.
Act on the object or method under test with the arranged parameters.
Assert (or verify) that the action of the object or method under test behaves as expected.
Let's take 3A into practice with a simple example.
Let's develop a small calculator program that can be used anywhere from a console application and in the future from a web application or even from a mobile application. We will focus on the calculator logic to understand the needs first, then go over the tests we can create to ensure it works as expected as we introduce other requirements later in this course.
We will start by modeling the logic from a Calculator class, which will be responsible for performing all the operations that we will need, including add, subtract, multiply, and divide. We will have specific methods for each operation, and we will start with the Add
method first.
The Calculator class will be part of the CalculatorProgram project, which will be a basic console application by now. We can start with the Add method body as the following:
using System;
using System.Linq;
namespace CalculatorProgram
{
public class Calculator
{
public decimal Sum(params decimal[] numbers)
{
var result = 0.0M;
for (int i = 0; i < numbers.Length; i++)
{
result += numbers[i];
}
return result;
}
}
}
From a set of numbers to be passed, the Add
method can produce a result that can be output to the caller program. By creating the xUnit test project with Visual Studio, we can add a reference to the CalculatorProgram and take the default UnitTest1.cs file to compose our first unit test with the 3A pattern as follows:
using System;
using CalculatorProgram;
using Xunit;
namespace UnitTests
{
public class UnitTest1
{
[Fact]
public void Test_AddTwoElements()
{
// 1) Arrange
var calculator = new Calculator();
// 2) Act (the actual operation)
var result = calculator.Sum(1, 1);
// 3) Then, Assert
Assert.Equal(2, result);
}
}
}
In the arrange step, you set the necessary dependencies to start testing your component.
In the act step, you take action into the call that is expected to do the work.
Finally, with the assert step, you can check the results to ensure the correct action took place. The first parameter passed to the Assert.Equal
method corresponds to the expected value, and the second parameter is intended to pass the evaluating result.
Learn the Assert class methods
The Assert class contains a set of very useful methods that each tests a specific condition. For example:
True
,False
Equal
,NotEqual
,NotSame
(equality comparisons)Null
,NotNull
,IsType
,IsNotType
(null and type checks)Contains
,Single
,Empty
(collection checks)
Methods defined in the Assert class are pretty easy to follow and well-documented in terms of the parameters they expect and how they can be used.
The most used methods for assertions include Equals
, NotEquals
, True
, and False
, which are simple but powerful for evaluating the results from a given component. For more complex scenarios or interactions between multiple components in your application, they allow you to set the expected results among different combinations and possible results.
Expand your tests with non-happy path scenarios
Let's go over some other possible scenarios.
Generally, and as a good practice, when you identify the most common scenario (also known as the "happy path" scenario), you should also compose separate test cases for other possible scenarios that could derive from the main usage ("non-happy path" scenarios). Both happy path and non-happy path cases should be identified and expressed with separate test methods, or [Fact]s, that can set the boundaries of the behavior of your component.
For our calculator program, we would like to have the ability to sum as many elements as we want, so it will be good to have a separate scenario where we can send different values, like integers and decimals.
[Fact]
public void Test_AddManyElements()
{
// 1) Arrange
var calculator = new Calculator();
// 2) Act (the actual operation)
var result = calculator.Sum(0.5M, 1, 2, 3, 4, -5.5M);
// 3) Then, Assert
Assert.Equal(5, result);
Assert.IsType<decimal>(result);
}
By declaring a separate scenario, more specific code can be tailored around the problem and you can test for specific values that the system should take accordingly. The same can be done when new features are introduced to your program (like the subtract, multiply, and divide operations for our calculator program) and they can have their own set of test cases with specialized code that examine and expect the results from their operations.
Let's recap!
By following the 3A pattern, you can set the expected results for the component that you are building to ensure it works as expected.
If you make a mistake, use a bad approach or a bad calculation, miss a significant step in your logic, or give rise to unintended side effects in your code, assertions can help you to identify the problem and correct it.
Account for both happy path and non-happy path cases to get fully covered as you continue developing and bringing more features to your program.