Kontorirotid

Veebilehtede tegemine

Unit Tests

|

Writing Unit Tests for your MVC .NET project

Why testing your application?

Unit Test writing is something every developer would come into someday. It keeps code finalized, it double checks whether code works as expected. There is even an acronym TTD – Test Driven Development, where they say you should write tests first and then your code. We with our application we do our tests now. If you are new to this tutorial, you can download current snapshot of our project here and continue with creating Unit Tests for it.

What to test?

There are several things we could write tests for. Integration test to see how different parts of code work together. Then there are load testing to test website’s performance. Then there are also front-end tests to check if your webpages contain some elements. We with our project are going to test our newly made Service class – lets test if it works as expected.

NUnit test framework

Two most popular frameworks for .NET tests are NUnit and xUnit test framework. Theres not that much difference between them, but on NUnit I like that if you create many TestCases for one method, then there is an option to give a name to that Testcase.

So lets add a new NUnit test project. I personally like if testing project is last one on folder list, so lets name our new project as zUnit so that it will be below WebApp project in folder structure.

Click on your project Solution -> Add -> New project. Search for NUnit Test Project. Click Next and give a name of zUnitTests for this project.

Test plan

I’m thinking about testing TodosService. This Service has GetAllAsync method that needs some testData to test search functionality. And then there are other CRUD methods that we could test without test data. And then our Service class has that _context thing that retrieves data from db. We don’t want to use real database, instead, lets use InMemody Database for our testing.

Lets first create some files. We need a file for test data, file to setup InMemory database, and 2 test files: one for GetAllAsync method and other for other tests. Lets keep test data in separate folder. So lets create new folder where we write a json file, lets call it todo-test-data.json

Files that we would create are as follows

Data/todo-test-data.json
TodoAddEditDeleteTest.cs
TodoGetAllTest.cs
SetupInMemoryDb.cs

If they are created then your unit test project should look like this:

Creating test data

Lets populate todo-test-data.json file first. I like to populate test data like this: First create data in Excel, save it as csv file and then use some csv-to-json converters that are freely available on internet. For example I created this kind of test data with Excel:

Data has 5 elements, everyone have unique GUID identifier created by a generator. First and second element have both “foo” on their description, so we can test whether search functionality would return both uppercase and lowercase values. Test data items have different numeric values for Status so we can test filtering by Status. For all items DueDates are future values, but same most earliest date is for 2 of them – If dueDates are same then lets sort them by Title.

When we are ready with creating our .csv file for testData, lets convert it to json format. Im using convertcsv.com. Output for our test data is here, copy this to todo-test-data.json file:

[
 {
   "Id": "92e8f9c8-a8a9-4f33-95fb-fae1776393e7",
   "Title": "Entity1",
   "Description": "contains 'foo'",
   "DueDate": "01.01.2056",
   "Status": 0
 },
 {
   "Id": "911b761d-a2e7-4b28-ab5c-b2315edfd020",
   "Title": "Entity2",
   "Description": "'FOO' uppercase",
   "DueDate": "01.01.2057",
   "Status": 0
 },
 {
   "Id": "6a3b98d9-f0b3-4bc2-bf42-92e716a2e16a",
   "Title": "Entity3",
   "Description": "Same date, second sort by Title. Should be first with Date search",
   "DueDate": "01.01.2055",
   "Status": 1
 },
 {
   "Id": "ee7b8be9-cead-4abd-8968-6c4530067f57",
   "Title": "Entity4",
   "Description": "Same date, second sort by Title. Should be second with Date search",
   "DueDate": "01.01.2055",
   "Status": 1
 },
 {
   "Id": "069ac0d1-6558-4529-9b74-bdc45d45aeed",
   "Title": "Entity5",
   "Description": "only Completed one",
   "DueDate": "01.01.2058",
   "Status": 2
 }
]

.NET do not automatically copy new .json files to output, so we have to set it manually. Right click on our JSON file -> Properties -> Copy to Output Directory -> Copy if never. Snapshot of this setup is shown here:

Set up In Memory database

Next thing we need to do is to set up In Memory database. Since we will use Data for GetAllAsync method, but not for other CRUD methods, then we need our method to know if to add test data or not. Copy this code to SetupInMemoryDb.cs:

// SetupInMemoryDb.cs
using Domain;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;

namespace zUnitTests
{
    public class SetupInMemoryDb
    {
        /// <summary>
        /// Creates options to use InMemory database for testing. 
        /// </summary>
        /// <param name="DbName">Name of your database. Eg 'test database'</param>
        /// <param name="withTestData">Initialize database with some test data</param>
        /// <returns>Options to initialise EFCore.AppDbContext </returns>
        public static DbContextOptions<AppDbContext> Setup(string DbName, bool withTestData)
        {
            var options = new DbContextOptionsBuilder<AppDbContext>()
                .UseInMemoryDatabase(databaseName: DbName)
                .Options;
            if (withTestData)
            {
                var todos = GetTestTodos();
                using (var context = new AppDbContext(options))
                {
                    foreach (var todo in todos) { context.Todos.Add(todo); }
                    context.SaveChanges();
                }
            }
            return options;
        }
        private static List<Todo> GetTestTodos()
        {
            string todosJson = File.ReadAllText("Data/todo-test-data.json");
            return JsonConvert.DeserializeObject<List<Todo>>(todosJson)!;
        }
    }
}

For this code to compile you need to:

*Install package ‘Microsoft.EntityFrameworkCore.InMemory‘ from nuget package manager

*Project reference to Domain project (Service project reference is needed with next code snippets)

Writing Data access test cases

Now we are finally ready to write test cases. Fill in your TodoGetAllTests.cs with code shown here:

using Domain;
using Services;

namespace zUnitTests
{
    [TestFixture]
    public class TodoGetAllTest
    {
        private AppDbContext _context;
        private TodoService _service;

        [OneTimeSetUp]
        public void OneTimeSetup()
        {
            var options = SetupInMemoryDb.Setup("Test_Get_Method", true);
            _context = new AppDbContext(options);
            _service = new TodoService(_context);
        }

        [OneTimeTearDown]
        public void OneTimeTearDown()
        {
            _context.Dispose();
        }

        [Test]
        [TestCase("", 5, TestName = "Empty keyword should return all values")]
        [TestCase("Entity", 5, TestName = "Test keyword in title")]
        [TestCase("foo", 2, TestName = "Case-insensitive lovercase search")]
        [TestCase("FOO", 2, TestName = "Case-insensitive uppercase search")]
        public async Task GetAll_By_SearchKeyword(string searchKeyword, int expectedResult)
        {
            var result = await _service.GetAllAsync(searchKeyword, null, null);

            Assert.That(result.Count(), Is.EqualTo(expectedResult));
        }

        [Test]
        [TestCase("InProgress", 2, TestName = "Filter InProgress")]
        [TestCase("Pending", 2, TestName = "Filter Pending")]
        [TestCase("Completed", 1, TestName = "Filter Completed")]
        public async Task GetAll_By_StatusFilter(string statusFiler, int expectedResult)
        {
            var result = await _service.GetAllAsync(null, statusFiler, null);

            Assert.That(result.Count, Is.EqualTo(expectedResult));
        }

        [Test]
        [TestCase("Status", "Entity1", "Entity3", TestName = "Sort by status")]
        [TestCase("DueDate", "Entity3", "Entity1", TestName = "Sort by due date")]
        public async Task GetAll_By_SortBy(string sortBy, string firstResultTitle, string thirdResultTitle)
        {
            IEnumerable<Todo> result = await _service.GetAllAsync(null, null, sortBy);

            Assert.That(result.ToList()[0].Title, Is.EqualTo(firstResultTitle));
            Assert.That(result.ToList()[2].Title, Is.EqualTo(thirdResultTitle));
        }
    }
}

Now you can right click TextFixture on top and select Run Tests. If all went good you should see green lights that all are passing:

Unit Test CRUD functionality

Now finally lets finish up testing whit writing unit tests for remaining TodoService methods. Populate TodoAddEditDeleteTest.cs with following content:

namespace zUnitTests
{
    [TestFixture]
    public class TodoAddEditDeleteTest
    {
        private AppDbContext _context;
        private TodoService _service;

        [OneTimeSetUp]
        public void OneTimeSetup()
        {
            var options = SetupInMemoryDb.Setup("Test_Add_Edit_Delete", true);
            _context = new AppDbContext(options);
            _service = new TodoService(_context);
        }

        [OneTimeTearDown]
        public void OneTimeTeardown()
        {
            _context.Dispose();
        }

        [Test]
        public async Task Add_Edit_Delete_Workflow()
        {
            // 1. succesfull creation
            var todoTask = await _service.CreateAsync(todo1);

            // Assert that the task was succesfully created and has a valid ID
            Assert.NotNull(todoTask);
            Assert.That(todoTask.Id, Is.Not.EqualTo(Guid.Empty));

            // Assert that the task exists in the database 
            var createdTask = await _service.GetByIdAsync(todoTask.Id);
            Assert.That(createdTask, Is.Not.Null);
            Assert.That(createdTask.Title, Is.EqualTo(todo1.Title));

            // 2. Edit some values in that task
            todoTask.DueDate = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            todoTask.Status = Status.Completed;

            // Updating can be with past time 
            var updatedTask = await _service.UpdateAsync(todoTask);
            Assert.That(updatedTask, Is.Not.Null);
            Assert.That(updatedTask.DueDate, Is.EqualTo(new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)));
            Assert.That(updatedTask.Status, Is.EqualTo(Status.Completed));

            // 3. Delete the task
            await _service.DeleteAsync(todoTask.Id);

            // Assert that the task was succesfully deleted
            var deletedTask = await _service.GetByIdAsync(todoTask.Id);
            Assert.That(deletedTask, Is.Null);
        }

        [Test]
        public void DueDate_Past_Will_Throw_Error()
        {
            var exception = Assert.ThrowsAsync<InvalidOperationException>(async () =>
            {
                await _service.CreateAsync(todo2);
            });

            Assert.That(exception.Message, Is.EqualTo("DueDate cannot be in the past"));
        }
        Todo todo1 = new Todo()
        {
            Title = "task with future date",
            Description = "bla bla bla",
            DueDate = new DateTime(2050, 1, 1, 0, 0, 0, DateTimeKind.Utc),
            Status = Status.InProgress,
        };

        static Todo todo2 = new Todo()
        {
            Title = "task with past date",
            Description = "bla bla bla",
            DueDate = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc),
            Status = Status.Pending,
        };
    }
}

Thats it. If you run your tests then verify that everything works again:

Summary

That’s it about this tutorial. If you got it this far then Well Done! Leave comments how you liked this tutorial. See you next time.

Leave a Reply

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