Our website uses cookies to ensure the quality of the services you provide. By continuing to browse, you agree to our cookie policy. Learn more

Let’s have some fun with unit tests!

Date: 2020-03-12 Author: Ignas Smirinėnka

Tutorial: Learn different ways to create a unit test cases using NUnit and C# with code samples.

As a developer, given the best practices of coding, it is vital to write unit test cases, to ensure that every piece of code works for all scenarios. Also, it makes functional and user acceptance testing a breeze.

There are innumerable ways to write a unit test. For this case, we’ll be using NUnit + MOQ library combination. These libraries allow you to automate the test cases, thereby making sure you don’t miss any scenarios.  

Let us consider an example, a Library Management System, to discuss the different ways of writing unit cases and evaluate each option. 

 

Scenario: create unit tests in a Library Management System

Assume that you have a library and your application is tracking the books borrowed by persons.

 

To keep things simple, let us have these entities:

  1. Books: to store the books that are present in the library.
  2. Persons: to save the list of library members.
  3. Lendings: to map books to persons and when they lent/returned the books.

 

https://github.com/koditus/advanced-unit-tests/blob/master/Services/Models/Book.cs

public class Book
{
    public int Id { get; set; }
    public string Name { get; set; }
}

https://github.com/koditus/advanced-unit-tests/blob/master/Services/Models/Person.cs

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string EmailAddress { get; set; }
}

https://github.com/koditus/advanced-unit-tests/blob/master/Services/Models/Lending.cs

public class Lending
{
    public int Id { get; set; }
    public Book Book { get; set; }
    public Person Person { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime? EndDate { get; set; }
}

 

SetUp using NUnit

To begin, we first need to set up our SUT (system under test, and dependency mocks). This is done using the method with attribute [SetUp].

SetUp defines the function that we need to call before each unit test. For example, in our library management system, I can use set up to initialize a repository for books, persons and lending.

NUnit documentation: https://github.com/nunit/docs/wiki/SetUp-Attribute

https://github.com/nunit/docs/wiki/SetUp-and-TearDown

 

https://github.com/koditus/advanced-unit-tests/blob/master/UnitTests/LendingServiceTestsBase.cs

public class LendingServiceTestsBase
{
    protected const int ValidBookId = 1;
    protected const int InvalidBookId = 6;
    protected const int ValidPersonId = 1;
    protected const int InvalidPersonId = 6;
    protected const int LentBookId = 7;
    protected const int ReturnedBookId = 8;

    protected LendingService LendingService;

    private Dictionary<int, Lending> _lendings;

    private Mock<ILendingRepository> _mockLendingRepository;
    private Mock<IBookRepository> _mockBookRepository;
    private Mock<IPersonRepository> _mockPersonRepository;

    private int NextLendingId => _lendings.Keys.Append(0).Max() + 1;

    [SetUp]
    public void SetUp()
    {
        _lendings = new Dictionary<int, Lending>
        {
            {
                1, new Lending
                {
                    Book = new Book
                    {
                        Id = LentBookId
                    },
                    EndDate = null,
                    Id = 1,
                    Person = new Person
                    {
                        Id = ValidPersonId
                    }
                }
            }
        };

        _mockLendingRepository = new Mock<ILendingRepository>();
        _mockLendingRepository.Setup(repo => repo.GetAll()).Returns(() => _lendings.Values.ToArray());
        _mockLendingRepository.Setup(repo => repo.Add(It.IsAny<Lending>()))
            .Callback<Lending>(lending =>
            {
                lending.Id = NextLendingId;
                _lendings.Add(lending.Id, lending);
            })
            .Returns((Lending lending) => lending);
        _mockLendingRepository.Setup(repo => repo.Update(It.IsAny<Lending>()))
            .Callback<Lending>(lending => _lendings[lending.Id] = lending)
            .Returns<Lending>(lending => _lendings[lending.Id]);

        _mockBookRepository = new Mock<IBookRepository>();
        _mockBookRepository.Setup(repo => repo.GetOne(ValidBookId))
            .Returns((int bookId) => new Book { Id = bookId });
        _mockBookRepository.Setup(repo => repo.GetOne(LentBookId))
            .Returns((int bookId) => new Book { Id = bookId });
        _mockBookRepository.Setup(repo => repo.GetOne(ReturnedBookId))
            .Returns((int bookId) => new Book { Id = bookId });
        _mockBookRepository.Setup(repo => repo.GetOne(InvalidBookId))
            .Returns<Book>(null);

        _mockPersonRepository = new Mock<IPersonRepository>();
        _mockPersonRepository.Setup(repo => repo.GetOne(ValidPersonId))
            .Returns((int parameter) => new Person { Id = parameter });
        _mockPersonRepository.Setup(repo => repo.GetOne(InvalidPersonId))
            .Returns<Person>(null);

        LendingService = new LendingService(_mockLendingRepository.Object, _mockBookRepository.Object,
            _mockPersonRepository.Object);
    }

    protected void AssertBookIsValid(Lending lending, int bookId, int personId)
    {
        Assert.IsNotNull(lending);
        Assert.AreEqual(bookId, lending.Book.Id);
        Assert.AreEqual(personId, lending.Person.Id);
    }

    protected void AssertBookLent(Lending lending, int bookId, int personId)
    {
        AssertBookIsValid(lending, bookId, personId);
        Assert.IsNotNull(lending.StartDate);
        Assert.IsNull(lending.EndDate);
    }

    protected void AssertBookReturned(Lending lending, int bookId, int personId)
    {
        AssertBookIsValid(lending, bookId, personId);
        Assert.IsNotNull(lending.StartDate);
        Assert.IsNotNull(lending.EndDate);
    }

    protected void VerifyNoOtherCalls()
    {
        _mockBookRepository.VerifyNoOtherCalls();
        _mockLendingRepository.VerifyNoOtherCalls();
        _mockPersonRepository.VerifyNoOtherCalls();
    }

    protected void VerifyBookRepository_GetOne_IsCalled(int times)
    {
        _mockBookRepository.Verify(mock => mock.GetOne(It.IsAny<int>()), Times.Exactly(times));
    }

    protected void VerifyPersonRepository_GetOne_IsCalled(int times)
    {
        _mockPersonRepository.Verify(mock => mock.GetOne(It.IsAny<int>()), Times.Exactly(times));
    }

    protected void VerifyLendingRepository_GetAll_IsCalled(int times)
    {
        _mockLendingRepository.Verify(mock => mock.GetAll(), Times.Exactly(times));
    }

    protected void VerifyLendingRepository_Add_IsCalled(int times)
    {
        _mockLendingRepository.Verify(mock => mock.Add(It.IsAny<Lending>()),
            Times.Exactly(times));
    }

    protected void VerifyLendingRepository_Update_IsCalled(int times)
    {
        _mockLendingRepository.Verify(mock => mock.Update(It.IsAny<Lending>()),
            Times.Exactly(times));
    }
}

 

Similar to Set Up, there is the TearDown attribute to run the functions after each unit test.

NUnit documentation: https://github.com/nunit/docs/wiki/TearDown-Attribute

https://github.com/nunit/docs/wiki/SetUp-and-TearDown

Adding Test Cases

Now that we have set up, let’s start adding our unit tests. 

 

To create a test case an attribute [TestFixture] is added to the class containing the test functions. A similar attribute [Test] is added to the functions.

 

NUnit documentation links:

https://github.com/nunit/docs/wiki/TestFixture-Attribute

https://github.com/nunit/docs/wiki/Test-Attribute

 

Step 1:

Let’s check our primary “happy path” scenario, when an existing book is lent to an existing person.

 

https://github.com/koditus/advanced-unit-tests/blob/master/UnitTests/ExampleOne.cs

[Test]
public void GivenValidBookAndPersonIds_WhenLendingBookToAPerson_ThenBookGetsLent()
{
    //Arrange

    //Act
    var lending = LendingService.Lend(ValidBookId, ValidPersonId);

    //Assert
    AssertBookIsValid(lending, ValidBookId, ValidPersonId);

    VerifyBookRepository_GetOne_IsCalled(1);
    VerifyPersonRepository_GetOne_IsCalled(1);
    VerifyLendingRepository_GetAll_IsCalled(1);
    VerifyLendingRepository_Add_IsCalled(1);

    VerifyNoOtherCalls();
}

 

Step 2:

But what about if a person and a book doesn’t exist? Let’s test it!

 

https://github.com/koditus/advanced-unit-tests/blob/master/UnitTests/ExampleOne.cs

[Test]
public void GivenInvalidBookAndPersonIds_WhenLendingBookToAPerson_ThenGetException()
{
    //Arrange

    //Act / Assert
    Assert.Throws<InvalidOperationException>(() => LendingService.Lend(InvalidBookId, InvalidPersonId));

    VerifyBookRepository_GetOne_IsCalled(1);

    VerifyNoOtherCalls();
}

 

Step 3:

But what about, if a person exists, but a book doesn’t? Or vice-versa? Or both?

Or if the book is already lent?

 

https://github.com/koditus/advanced-unit-tests/blob/master/UnitTests/ExampleTwo.cs

namespace UnitTests
{
    [TestFixture]
    public class ExampleTwo : LendingServiceTestsBase
    {
        [Test]
        public void GivenBookAlreadyLent_WhenLendingAgain_ThenGetException()
        {
            //Arrange

            //Act
            LendingService.Lend(ValidBookId, ValidPersonId);

            //Assert
            Assert.Throws<InvalidOperationException>(() => LendingService.Lend(ValidBookId, ValidPersonId));

            VerifyBookRepository_GetOne_IsCalled(2);
            VerifyPersonRepository_GetOne_IsCalled(2);
            VerifyLendingRepository_GetAll_IsCalled(2);
            VerifyLendingRepository_Add_IsCalled(1);

            VerifyNoOtherCalls();
        }


        [Test]
        public void GivenInvalidBookIdAndValidPersonId_WhenLendingBookToAPerson_ThenGetException()
        {
            //Arrange

            //Act / Assert
            Assert.Throws<InvalidOperationException>(() => LendingService.Lend(InvalidBookId, ValidPersonId));

            VerifyBookRepository_GetOne_IsCalled(1);

            VerifyNoOtherCalls();
        }

        [Test]
        public void GivenValidBookIdAndInvalidPersonId_WhenLendingBookToAPerson_ThenGetException()
        {
            //Arrange

            //Act / Assert
            Assert.Throws<InvalidOperationException>(() => LendingService.Lend(ValidBookId, InvalidPersonId));

            VerifyBookRepository_GetOne_IsCalled(1);
            VerifyPersonRepository_GetOne_IsCalled(1);

            VerifyNoOtherCalls();
        }
    }
}

 

Step 4:

Now that we have lending functionality covered, let’s look at book returning functionality. Cases in which we are interested are: What if the book is lent and we want to return it(happy path)? Book is already returned and we try to return it again; We are trying to return non lent book;

 

https://github.com/koditus/advanced-unit-tests/blob/master/UnitTests/ExampleThree.cs

namespace UnitTests
{
    [TestFixture]
    public class ExampleThree : LendingServiceTestsBase
    {
        [Test]
        public void GivenBookAlreadyLent_WhenReturning_ThenBookGetsReturned()
        {
            //Arrange
            LendingService.Lend(ValidBookId, ValidPersonId);

            //Act
            var returnedLending = LendingService.Return(ValidBookId);

            //Assert
            AssertBookReturned(returnedLending, ValidBookId, ValidPersonId);

            VerifyBookRepository_GetOne_IsCalled(1);
            VerifyPersonRepository_GetOne_IsCalled(1);
            VerifyLendingRepository_GetAll_IsCalled(2);
            VerifyLendingRepository_Add_IsCalled(1);
            VerifyLendingRepository_Update_IsCalled(1);
        }

        [Test]
        public void GivenReturnedBook_WhenReturning_ThenGetException()
        {
            //Arrange
            LendingService.Lend(ValidBookId, ValidPersonId);
            LendingService.Return(ValidBookId);

            //Act / Assert
            Assert.Throws<InvalidOperationException>(() => LendingService.Return(ValidBookId));

            VerifyBookRepository_GetOne_IsCalled(1);
            VerifyPersonRepository_GetOne_IsCalled(1);
            VerifyLendingRepository_GetAll_IsCalled(3);
            VerifyLendingRepository_Add_IsCalled(1);
            VerifyLendingRepository_Update_IsCalled(1);
        }

        [Test]
        public void GivenNonLentBook_WhenReturning_ThenGetException()
        {
            //Arrange

            //Act / Assert
            Assert.Throws<InvalidOperationException>(() => LendingService.Return(ValidBookId));

            VerifyLendingRepository_GetAll_IsCalled(1);

            VerifyNoOtherCalls();
        }

    }
}

 

Step 5:

At the current state we have main functionality covered. Now can we make it simpler? Let’s give it a try by looking into lending functionality when a book or person is invalid and combine all those cases into one unit test using the [TestCase] attribute.

 

https://github.com/koditus/advanced-unit-tests/blob/master/UnitTests/ExampleFour.cs

namespace UnitTests
{
    [TestFixture]
    public class ExampleFour : LendingServiceTestsBase
    {
        [Test]
        [TestCase(InvalidBookId, ValidPersonId)]
        [TestCase(ValidBookId, InvalidPersonId)]
        [TestCase(InvalidBookId, InvalidPersonId)]
        public void GivenInvalidBookOrPersonIds_WhenLendingBookToAPerson_ThenGetException(int bookId, int personId)
        {
            //Arrange

            //Act / Assert
            Assert.Throws<InvalidOperationException>(() => LendingService.Lend(bookId, personId));

            VerifyBookRepository_GetOne_IsCalled(1);
            VerifyPersonRepository_GetOne_IsCalled(bookId == InvalidBookId ? 0 : 1);

            VerifyNoOtherCalls();
        }
    }
}

NUnit documentation links:

https://github.com/nunit/docs/wiki/TestCase-Attribute

 

Step 6:

For keeping consistency we also rework the positive case with [TestCase] attribute even so it wouldn’t bring any value at this exact case.

 

https://github.com/koditus/advanced-unit-tests/blob/master/UnitTests/ExampleFive.cs

 [Test]
        [TestCase(ValidBookId, ValidPersonId)]
        public void GivenBookAndPersonIds_WhenLendingBookToAPerson_ThenLendsWithValidationChecks(int bookId,
            int personId)
        {
            //Arrange

            //Act
            var lending = LendingService.Lend(bookId, personId);

            //Assert
            AssertBookLent(lending, ValidBookId, ValidPersonId);

            VerifyBookRepository_GetOne_IsCalled(1);
            VerifyPersonRepository_GetOne_IsCalled(1);
            VerifyLendingRepository_GetAll_IsCalled(1);
            VerifyLendingRepository_Add_IsCalled(1);

            VerifyNoOtherCalls();
        }
    }
}

 

Step 7:

For trying to reach simplicity we used attributes and one may say that our unit tests actually became simpler. But sometimes it happens that you just get carried away with the idea of squeezing things together or just by the simple wish to try things out. And the result gets very messy. Let’s try to hypothetically imagine such a situation and make a unit test which would have all the unit test scenarios in one test. For that we will use the [Theory] attribute.

 

https://github.com/koditus/advanced-unit-tests/blob/master/UnitTests/ExampleSix.cs

namespace UnitTests
{
    [TestFixture]
    public class ExampleSix : LendingServiceTestsBase
    {
        [Test]
        [Theory]
        public void GivenBookStatus_WhenPerformingAction_ThenActionIsPerformedWithValidations(bool isLent,
            bool isReturned, bool isLending)
        {
            //Arrange
            if(isLent || isReturned)
                LendingService.Lend(ValidBookId, ValidPersonId);

            if(isReturned)
                LendingService.Return(ValidBookId);

            //Act
            Lending lending = null;
            Lending returnedLending = null;
            if(isLending)
            {
                if(isLent && !isReturned)
                    Assert.Throws<InvalidOperationException>(() => LendingService.Lend(ValidBookId, ValidPersonId));

                if(!isLent)
                    lending = LendingService.Lend(ValidBookId, ValidPersonId);
            }
            else
            {
                if(isReturned || !isLent)
                    Assert.Throws<InvalidOperationException>(() => LendingService.Return(ValidBookId));

                if(!isReturned && isLent)
                    returnedLending = LendingService.Return(ValidBookId);
            }

            //Assert
            if(isLending)
            {
                if(!isLent)
                {
                    Assert.IsNotNull(lending);
                    Assert.AreEqual(ValidBookId, lending.Book.Id);
                    Assert.AreEqual(ValidPersonId, lending.Person.Id);
                    Assert.IsNotNull(lending.StartDate);
                }
            }
            else
            {
                if(!isReturned && isLent)
                {
                    Assert.IsNotNull(returnedLending);
                    Assert.IsNotNull(returnedLending.EndDate);
                }
            }
        }
    }
}

 

NUnit documentation links:

https://github.com/nunit/docs/wiki/Theory-Attribute

 

Step 8:

But what if we actually would try to push a little forward and make our all cases into simpler unit tests. Well one thing about making things simpler is that everyone may have a different understanding of what is simpler. So let’s try to make one possible way of many possibilities how our unit test’s can be simplified a little more. For that we will use [Sequential] and [Values] attributes.

 

https://github.com/koditus/advanced-unit-tests/blob/master/UnitTests/ExampleSeven.cs

namespace UnitTests
{
    [TestFixture]
    public class ExampleSeven : LendingServiceTestsBase
    {
        [Test, Sequential]
        public void GivenInvalidBookOrPerson_WhenLending_ThenValidations(
            [Values(ValidBookId, InvalidBookId, LentBookId, ReturnedBookId)] int bookId, 
            [Values(InvalidPersonId, ValidPersonId, ValidPersonId, InvalidPersonId)] int personId)
        {
            //Arrange

            //Act / Assert
            Assert.Throws<InvalidOperationException>(() => LendingService.Lend(bookId, personId));
        }

        [Test, Sequential]
        public void GivenValidBookAndPerson_WhenLending_ThenLent([Values(ValidBookId, ReturnedBookId)] int bookId,
            [Values(ValidPersonId, ValidPersonId)] int personId)
        {
            //Arrange

            //Act
            var lending = LendingService.Lend(bookId, personId);

            //Assert
            AssertBookLent(lending, bookId, personId);
        }

        [Test, Sequential]
        public void GivenInvalidBooksForReturn_WhenReturning_ThenValidations([Values(ValidBookId, InvalidBookId, ReturnedBookId)] int bookId)
        {
            //Arrange

            //Act / Assert
            Assert.Throws<InvalidOperationException>(() => LendingService.Return(bookId));
        }

        [Test, Sequential]
        public void GivenLentBook_WhenReturning_ThenBookReturned([Values(LentBookId)] int bookId)
        {
            //Arrange

            //Act / Assert
            var lending = LendingService.Return(bookId);

            //Assert
            AssertBookReturned(lending, bookId, ValidPersonId);
        }
    }
}

 

NUnit documentation links:

https://github.com/nunit/docs/wiki/Sequential-Attribute

https://github.com/nunit/docs/wiki/Values-Attribute

 

Conclusions :

There are many ways to write unit tests. There are many unit tests frameworks. We are not trying to say that our presented way is the best or even a good one. We just want to bring some attention to them, because we as developers highly value them. Unit tests saved us countless production releases, prevented many bugs and saved a lot of money for our clients.

 

 

About the author
Ignas Smirinėnka .NET Developer
https://www.linkedin.com/in/ignassmirinenka/

Other services

.NET Software Development

Read more >

.NET Specialist Sourcing and Recruitment

Read more >

.NET Team Extension

Read more >

.NET consulting

Read more >

Get a quote

Tell us about your challenge.
+

Get a quote

Tell us about your challenge.
Name
Company (optional)
Email
Phone number
Challenge/Message
Thank you for your message!