.NET with NUnit Test

What is NUnit?

NUnit is a unit-testing framework for all .Net languages. Initially ported from JUnit, the current production release, version 3, has been completely rewritten with many new features and support for a wide range of .NET platforms.

NuGet Packages

  1. NUnit
  2. NUnit3TestAdapter
  3. Microsoft.NET.Test.Sdk

Your First NUnit Test Case

Add [TestFixture] and [Test] to mark code as tests

Test can be run in Test Explorer and in Command Line

Why Write Automated Tests?

Help to find defects and regressions. When we make a change to the project, we may find that unintentionally break one of the existing tests. Something that once working is no longer working.

Automated Tests give us greater confidence that the software is working as it should.

Understanding the NUnit Test Framework

  • NUnit Library

    • Attributes e.g. [Test]
    • Assertions
  • Test Runner

    • Recognizes attributes
    • Execute test methods
    • Report test results
    • Test explorer
    • donet test

NUnit attributes Overview

  1. [TestFixture]: Mark a class that contains tests
  2. [Test]: Mark a method as a test
  3. [Category]: Organize tests into categories
  4. [TestCase]: Data driven test cases
  5. [Values]: Data driven test parameters
  6. [Sequential]: How to combine test data
  7. [SetUp]: Run code before each test
  8. [OneTimeSetUp]: Run code before first test in class

NUnite Assertions Overview

1
2
3
// Constraint Model of assertions (newer)
Assert.That(sut.Years, Is.EqualTo(1));
Assert.That(test result, constraint instance);

This Classic Model is still supported but since no new features have been added to it for some time. the constraint-based model must be used in order to have full access to NUnit’s capabilities.

1
2
3
4
Classic Model of assertions (older)
Assert.AreEqual(1, sut.Years);
Assert.NotNull(sut.Years);
Assert.xyz(...);

The Logical Arrange, Act, Assert Test Phases

  1. Arrange: Set up test objects, initialize test data
  2. Act: call methods, set property, to cause some effect in the project
  3. Assert: compare returned value/end state with expected

Qualities of Good Tests

  • Fast
  • Repeatable
  • Isolated: One Test should not depend on others to run
  • Trustworthy
  • Valuable

Asserting on Different Types of Results

Asserts: Evaluate and verify the outcome of a test based on a returned result, final object state, or the occurence of events observed during execution. An assert should either pass or fail.

How many asserts per test?

A single test usually focuses on testing a single ‘behaviour’. Multiple asserts are usually ok if all the asserts are related to testing this single behaviour.

Asserting on Equality

1
2
3
4
5
6
7
// compare value
Assert.That(a, Is.EqualTo(...));
Assert.That(a, Is.Not.EqualTo(...));

// compare reference
Assert.That(a, Is.SameAs(...));
Assert.That(a, Is.Not.SameAs(...));

Adding custom failure message

1
Assert.That(a, Is.EqualTo(...), "Custom Error Message");

Asserting on Floating Numbers

1
2
Assert.That(a, Is.EqualTo(0.33).Within(0.001));
Assert.That(a, Is.EqualTo(0.33).Within(10).Percent);

Asserting on Null Values

1
2
3
4
string name = "yuan";

Assert.That(name, Is.Null); // fail
Assert.That(name, Is.Not.Null); // pass

Asserting on String Values

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
string name = "yuan";

Assert.That(name, Is.Empty); // fail
Assert.That(name, Is.Not.Empty); // pass

Assert.That(name, Is.EqualTo("yuan")); // pass
Assert.That(name, Is.EqualsTo("YUAN")); // fail, case-sensitive
Assert.That(name, Is.EqualTo("YUAN").IgnoreCase); // pass

Assert.That(name, Does.StartWith("yu")); // pass
Assert.That(name, Does.EndWith("an")); // pass
Assert.That(name, Does.Contain("ua)); // pass
Assert.That(name, Does.Not.Contain("kk")); // pass
Assert.That(name, Does.StartWith("yu")
.And
.EndWith("an")); // pass
Assert.That(name, Does.StartWith("kk")
.Or
.EndWith("an")); // pass

Asserting on Boolean Values

1
2
3
4
5
6
7
8
9
10
bool isTrue =  true;

Assert.That(isTrue); // pass
Assert.That(isTrue, Is.True); // pass

bool isFalse = false;

Assert.That(isFalse == false); // pass
Assert.That(isFalse, Is.False); // pass
Assert.That(isFalse, Is.Not.True); // pass

Asserting within Ranges

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int i = 42;

Assert.That(i, Is.GreaterThan(42)); // fail
Assert.That(i, Is.GreaterThanOrEqualTo(42)); // pass
Assert.That(i, Is.LessThan(42)); // fail
Assert.That(i, Is.GreaterThanOrEqualTo(42)); // pass
Assert.That(i, Is.InRange(40, 50)); // pass

DateTiem d1 = new DateTime(2021, 2, 20);
DateTiem d2 = new DateTime(2021, 2, 25);

Assert.That(d1, Is.EqualTo(d2)); // fail
Assert.That(d1, Is.EqualTo(d2).Within(4).Days); // fail
Assert.That(d1, Is.EqualTo(d2).Within(5).Days); // pass

Asserting on Objects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Product {
int ProductId {get; set;}
string ProductName {get; set;}

Product(int ProductId, string ProductName) {
this.ProductId = ProductId;
this.ProductName = ProductName;
}
}

var products = new List<Product> {
new Product(1, "a"),
new Product(2, "b"),
};

Assert.That(products, Has.Exactly(2).Items); // pass
Assert.That(products, Is,Unique); // pass
Assert.That(products, Has.Exactly(1)
.Property("ProductName").EqualTo("a")
.And
.Property("ProductId).EqualTo(1));

Assert.That(products, Has.Exactly(1)
.Matches<Product>(
item => item.ProductName == "a" &&
item.ProductId == 1
));

Controlling Test Execution

Use [Ignore] to skip tests. [Ignore] could also be put before class to skip the entire test class

1
2
3
4
5
[Test]
[Ignore("Custom reason why we need to skip this test")]
public void TestWillNotRun() {

}

Use [Category] to add test cases to categories, we can only run tests for certain category.

In Test Explorer, we can group tests by Traits, which is just another name for Category

One Test Case can belongs to multiple [Category].

[Category] can be applied to Class

1
2
3
4
5
[Test]
[Category("Category 1")]
public void TestInCategoryOne() {

}

[SetUp] code will be executed before each test. So it is a good place to define variables and objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestClass {
private List<Product> products;
private string test;

[SetUp]
public void Setup() {
products = new List<Products> {
new Product(1, "a"),
new Product(2, "b"),
};

test = "test";
}
}

[TearDown] code will be executed after each test, it is the place to dispose all unnecessary objects

1
2
3
4
5
6
7
8
public class TestClass {
[TearDown]
public void Setup() {
if (products != null) {
((IDisposable)products).Dispose();
}
}
}

[OneTimeSetUp] code will be executed once before the first test case. Define objects that will not be modified by test cases here.

[OneTimeTearDown] code will be executed once after the last test case. Dispose any objects here.

Data Driven Tests and Reducing Code Duplication

[TestCase]: If we want to run the same test but with different data, we could pass different variables into the test function.

1
2
3
4
5
6
7
8
9
10
11
12
13
[Test]
[TestCase(200_000, 6.5, 30, 1264.14)]
[TestCase(200_000, 10, 30, 1755.14)]
[TestCase(500_000, 10, 30, 4387.86)]
public void CalculateCorrectMonthlyRepayment(decimal principal, decimal interestRate, int termInYears, decimal expectedMonthlyPayment)
{
var sut = new LoanRepaymentCalculator();

var monthlyPayment = sut.CalculateMonthlyRepayment(
new LoanAmount("USD", principal), interestRate, new LoanTerm(termInYears));

Assert.That(monthlyPayment, Is.EqualTo(expectedMonthlyPayment));
}
1
2
3
4
5
6
7
8
9
10
[Test]
[TestCase(200_000, 6.5, 30, ExpectedResult = 1264.14)]
[TestCase(200_000, 10, 30, ExpectedResult = 1755.14)]
[TestCase(500_000, 10, 30, ExpectedResult = 4387.86)]
public decimal CalculateCorrectMonthlyRepayment_SimplifiedTestCase(decimal principal, decimal interestRate, int termInYears)
{
var sut = new LoanRepaymentCalculator();

return sut.CalculateMonthlyRepayment(new LoanAmount("USD", principal), interestRate, new LoanTerm(termInYears));
}

Create Test Case from Centralized Data Class

[TestCaseSource(typeof(Class_Name), "Function_Name")]

1
2
3
4
5
6
7
8
9
10
11
[Test]
[TestCaseSource(typeof(MonthlyRepaymentTestData), "TestCases")]
public void CalculateCorrectMonthlyRepayment_Centralized(decimal principal, decimal interestRate, int termInYears, decimal expectedMonthlyPayment)
{
var sut = new LoanRepaymentCalculator();

var monthlyPayment = sut.CalculateMonthlyRepayment(
new LoanAmount("USD", principal), interestRate, new LoanTerm(termInYears));

Assert.That(monthlyPayment, Is.EqualTo(expectedMonthlyPayment));
}

Create Test Case with Data from File

1
2
3
4
5
6
7
8
9
10
11
[Test]
[TestCaseSource(typeof(MonthlyRepaymentCsvData), "GetTestCases", new object[] { "Data.csv" })]
public void CalculateCorrectMonthlyRepayment_Csv(decimal principal, decimal interestRate, int termInYears, decimal expectedMonthlyPayment)
{
var sut = new LoanRepaymentCalculator();

var monthlyPayment = sut.CalculateMonthlyRepayment(
new LoanAmount("USD", principal), interestRate, new LoanTerm(termInYears));

Assert.That(monthlyPayment, Is.EqualTo(expectedMonthlyPayment));
}

Create Test Cases with Values, Sequential and Range

Without [Sequential], it will create 3 _ 3 _ 3 = 27 test cases
With [Sequential], it will only create 3 test cases

1
2
3
4
5
6
7
8
9
10
11
[Test]
[Sequential]
public void CalculateCorrectMonthlyRepayment_Combinatorial(
[Values(100_000, 200_000, 500_000)] decimal principal,
[Values(6.5, 10, 20)] decimal interestRate,
[Values(10, 20, 30)] int termInYears)
{
var sut = new LoanRepaymentCalculator();

var monthlyPayment = sut.CalculateMonthlyRepayment(new LoanAmount("USD", principal), interestRate, new LoanTerm(termInYears));
}

Create Custom Category Attribute

1
2
3
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
class ProductComparisonAttribute : CategoryAttribute
{}

Then we can use Custom Attribute like this

1
2
3
4
5
[Test]
[ProductComparison]
public void CustomAttributeTest() {

}