Monthly Archives: November 2020

Testy jednostkowe w Entity Framework Core 5

Testy są nieodłączną częścią tworzenia oprogramowania. Są to oddzielne programy, które pozwalają sprawdzić, czy napisany przez nas kawałek program robi dokładnie to, co powinien. Testy jednostkowe są małymi fragmentami kodu, które testują pojedyncze elementy programu a w Entity Framework Core 5 pisze się je zaskakująco łatwo.

W pamięci, czy nie

Microsoft zaleca, żeby przy pisaniu testów używających EF Core, używać prawdziwej bazy danych, jeżeli to tylko możliwe. W zasadzie najlepiej używać bazy danych dokładnie w tej samej konfiguracji i na tym samym serwerze, na którym ma działać nasza aplikacja finalnie. Takie podejście może nie mieć sensu ze względu na koszty, co przyznaje również Microsoft. Testy wydajnościowe z pewnością powinny sprawdzać nasze rozwiązania na środowisku możliwie zbliżonym do produkcyjnego. Natomiast przy pisaniu testów jednostkowych wystarczy nam trzymanie bazy danych w pamięci. Entity Framework Core pozwala na działanie na wirtualnej bazie danych, tworzonej jedynie w pamięci. Możemy tez wykorzystać bazę SQLite, ponieważ działa szybko i nie potrzebuje serwera. Ma także tryb, w którym może działać w pamięci. W tym rozdziale nie będziemy się wgłębiali w użycie SQLite do testów, jednak mogę Cię zapewnić, że nie wymaga to wiele zachodu.

Pisanie testów jednostkowych

Entity Framework Core 5 bardzo łatwo skonfigurować do działania w pamięci i w projekcie z testami, wystarczy doinstalować pakiet NuGet o nazwie Microsoft.EntityFrameworkCore.InMemory. Przydadzą się też inne pakiety, a oto ich cała lista:

  • Microsoft.EntityFrameworkCore.InMemory – to run EF Core 5 in memory
  • NUnit – a framework to write and run unit tests
  • NUnit3TestAdapter – an adapter to run NUnit tests in Visual Studio
  • FluentAssertions – easy library to write nice and readable assertions

Do testów użyję klasy ReservationController, którą po części pokazywałem już wcześniej. Oto jej pełna treść:

[ApiController]
[Route("[controller]")]
public class ReservationsController : ControllerBase
{
    private readonly PrimeDbContext primeDbContext;

    public ReservationsController(PrimeDbContext _primeDbContext)
    {
        primeDbContext = _primeDbContext;
    }

    [HttpGet]
    public async Task<IEnumerable<Reservation>> Get()
    {
        return await primeDbContext.Reservations.Include(r => r.Room).AsNoTracking().ToListAsync();
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(int id)
    {
        var reservation = await primeDbContext.Reservations.FindAsync(id);
        if (reservation == null)
        {
            return NotFound();
        }

        await primeDbContext.Entry(reservation).Collection(r => r.Profiles).LoadAsync();
        await primeDbContext.Entry(reservation).Reference(r => r.Room).LoadAsync();

        return Ok(reservation);
    }

    [HttpPost]
    public async Task<IActionResult> Post([FromBody] NewReservation newReservation)
    {
        var room = await primeDbContext.Rooms.FirstOrDefaultAsync(r => r.Id == newReservation.RoomId);
        var guests = await primeDbContext.Profiles.Where(p => newReservation.GuestIds.Contains(p.Id)).ToListAsync();

        if (room == null || guests.Count != newReservation.GuestIds.Count)
        {
            return NotFound();
        }

        var reservation = new Reservation
        {
            Created = DateTime.UtcNow,
            From = newReservation.From.Value,
            To = newReservation.To.Value,
            Room = room,
            Profiles = guests
        };

        var createdReservation = await primeDbContext.Reservations.AddAsync(reservation);
        await primeDbContext.SaveChangesAsync();

        return Ok(createdReservation.Entity.Id);
    }
}

Klasę testową nazwałem ReservationControllerTests, czyli jest to nazwa klasy i końcówka Tests na końcu. W tych testach skupię się na sprawdzeniu jak podmienić dane w Entity Framework Core, a nie aby przetestować wszystkie możliwe przypadki.

Podstawą tutaj jest odpowiednie przygotowanie PrimeDbContext do testowania. Sama podstawa klasy z testami wygląda następująco:

public class ReservationsControllerTests
{
    private DbContextOptions<PrimeDbContext> dbContextOptions = new DbContextOptionsBuilder<PrimeDbContext>()
        .UseInMemoryDatabase(databaseName: "PrimeDb")
        .Options;
    private ReservationsController controller;

    [OneTimeSetUp]
    public void Setup()
    {
        SeedDb();

        controller = new ReservationsController(new PrimeDbContext(dbContextOptions));
    }

    private void SeedDb()
    {
        using var context = new PrimeDbContext(dbContextOptions);
        var rooms = new List<Room>
        {
            new Room { Id = 1, Description = "Room nr 1", Number = 1, Level = 1, RoomType = RoomType.Standard },
            new Room { Id = 2, Description = "Room nr 2", Number = 2, Level = 1, RoomType = RoomType.Standard },
            new Room { Id = 3, Description = "Room nr 3", Number = 3, Level = 2, RoomType = RoomType.Suite }
        };

        var profiles = new List<Profile>
        {
            new Profile { Id = 1, Ref = "Profile 1", Forename = "Michał", Surname = "Białecki" },
            new Profile { Id = 2, Ref = "Profile 2", Forename = "John", Surname = "Show" },
            new Profile { Id = 3, Ref = "Profile 3", Forename = "Daenerys", Surname = "Targaryen" }
        };

        context.AddRange(rooms);
        context.AddRange(profiles);

        context.AddRange(new List<Reservation>
        {
            new Reservation
            { 
                Id = 1,
                Room = rooms[0],
                Profiles = new List<Profile>{ profiles[0] },
                From = DateTime.Today,
                To = DateTime.Today.AddDays(2)
            },
            new Reservation
            {
                Id = 2,
                Room = rooms[2],
                Profiles = new List<Profile>{ profiles[1], profiles[2] },
                From = DateTime.Today.AddDays(1),
                To = DateTime.Today.AddDays(3)
            }
        });

        context.SaveChanges();
    }
}

Pierwsza rzecz, która od razu zwaraca naszą uwagę, to metoda SeedDb, która służy do dodania danych testowych do kontektsu EF Core. Dla tych testów, dane zostaną wprowadzone tylko raz, na samym początku dzięki użyciu atrybutu [OneTimeSetUp]. Stan bazy danych zostanie zachowany dopóki działa proces, który te testy wykonuje. Jednak ważniejszy fragment znajduje się na samej górze, czyli utworzenie dbContextOptions. Zauważ, że to właśnie tam używamy opcji UseInMemoryDatabase, a następnie przy pomocy tego obiektu tworzymy klasę PrimeDbContext. Przy tworzeniu podajemy nazwę bazy danych i używamy zawsze tej samej.

Kolejna bardzo istotna linia to:

using var context = new PrimeDbContext(dbContextOptions);

Na początku używamy słowa kluczowego using, ponieważ nie chcemy aby Garbage Collector usunął nam zmienną context z pamięci w trakcie wykonywania testu.

Skoro mamy już skonfigurowaną bazę oraz dane, to czas na testy:

[Test]
public async Task Get_FetchesReservationsWithoutRoomsAndGuests()
{
    using var context = new PrimeDbContext(dbContextOptions);
    var reservations = (await controller.Get()).ToList();

    reservations.Count.Should().Be(2);
    reservations.All(r => r.Room == null).Should().BeFalse();
    reservations.All(r => r.Profiles == null).Should().BeTrue();
}

W pierwszym teście pobieramy wszystkie rezerwacje i sprawdzamy, czy ich zależności są załadowane. W tym przypadku tak nie będzie, ponieważ metoda Get w kontrolerze, nie wymusza ładowania zależności. Przetestujmy kolejną metodę.

[Test]
public async Task GetById_WhenIdIsProvided_FetchesReservationWithRoomsAndGuests()
{
    using var context = new PrimeDbContext(dbContextOptions);
    var result = await controller.GetById(2);
    var okResult = result.As<OkObjectResult>();
    var reservation = okResult.Value.As<Reservation>();

    reservation.Should().NotBeNull();
    reservation.Profiles.Should().NotBeNull();
    reservation.Room.Should().NotBeNull();
}

W drugim teście pobieramy pojedynczą rezerwację i tutaj sprawdzamy, że zarówno pokój, jak i profile są załadowane. Dzieje się tak, ponieważ w metodzie GetById używamy metod Collection i Reference, aby te zależności załadować. Przetestujmy teraz metodę Post.

[Test]
public async Task Post_WithRoomAndProfiles_AddsReservation()
{
    var newReservation = new NewReservation
    {
        From = DateTime.Today.AddDays(3),
        To = DateTime.Today.AddDays(7),
        RoomId = 3,
        GuestIds = new List<int> { 2 }
    };

    using var context = new PrimeDbContext(dbContextOptions);
    var result = await controller.Post(newReservation);

    var okResult = result.As<OkObjectResult>();
    var reservationId = okResult.Value.As<int>();
    var addedReservation = await context.Reservations
        .Include(p => p.Profiles)
        .Include(r => r.Room)
        .FirstOrDefaultAsync(r => r.Id == reservationId);

    addedReservation.Should().NotBeNull();
    addedReservation.Profiles.Should().NotBeNull();
    addedReservation.Profiles.Count.Should().Be(1);
    addedReservation.Profiles[0].Id.Should().Be(2);
    addedReservation.Room.Should().NotBeNull();
    addedReservation.Room.Id.Should().Be(3);
}

Ostatni test sprawdza, czy dodana rezerwacja, została dodana poprawnie. Sprawdzamy czy pokój i profil gościa, został odpowiednio przypisany do nowej rezerwacji.

Podsumowanie

Testy jednostkowe w Entity Framework Core piszę się naprawdę prosto i zrozumiale. Zaledwie kilka linii konfiguracji pozwala nam używać klasy dbContext do przygotowania żądanego stanu bazy danych. Nie musimy oddzielnie podmieniać poszczególnych kolekcji w PrimeDbContext jak to było w przypadku testów Entity Framework. Pod tym względem Entity Framework Core jest dopracowany, a testy jednostkowe z jego użyciem nie odstają znacząco od jakichkolwiek innych testów jednostkowych. Praca z nimi jest łatwa i przyjemna, czyli dokładnie tak, jak powinno być.

Cały zamieszczony tutaj kod dostępny jest ma moim GitHub, zerkaj do woli. Jest to projekt demonstracyjny co potrafi EF Core 5, więc zachęcam do eksploracji na własnę rękę!

Dzięki za przeczytanie i do zobaczenia 😊

 

Unit tests in Entity Framework Core 5

Tests are an integral part of software development. These are separate programs that allow you to check if a piece of the program written by us does exactly what it should. Unit tests are small pieces of code that test individual program elements and in Entity Framework Core 5 it’s surprisingly easy to write them.

In memory or not

Microsoft recommends that when writing tests that use EF Core, you should use a real database whenever possible. In fact, it is best to use the database in exactly the same configuration and on the same server on which our application is to run. This approach may not make sense when it comes to cost, as Microsoft also admits. Performance tests should certainly check our solutions in an environment as close to production as possible. However, when writing unit tests, it’s enough to keep the database in memory. Entity Framework Core allows you to run on a virtual database created only in memory. We can also use the SQLite database because it works fast and does not need a server. It also has a mode in which it can run in memory. In this chapter, we won’t go into detail about using SQLite for testing, but I can assure you that it doesn’t take much effort.

Writing unit tests

In Entity Framework Core 5 it’s very easy to configure the database to run in memory. In a test project, just install the NuGet package called Microsoft.EntityFrameworkCore.InMemory, but also a few more might come in handy. Let’s check the full list:

  • Microsoft.EntityFrameworkCore.InMemory – to run EF Core 5 in memory
  • NUnit – a framework to write and run unit tests
  • NUnit3TestAdapter – an adapter to run NUnit tests in Visual Studio
  • FluentAssertions – easy library to write nice and readable assertions

For testing, I will use the ReservationController class. Here is its full content:

[ApiController]
[Route("[controller]")]
public class ReservationsController : ControllerBase
{
    private readonly PrimeDbContext primeDbContext;

    public ReservationsController(PrimeDbContext _primeDbContext)
    {
        primeDbContext = _primeDbContext;
    }

    [HttpGet]
    public async Task<IEnumerable<Reservation>> Get()
    {
        return await primeDbContext.Reservations.Include(r => r.Room).AsNoTracking().ToListAsync();
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(int id)
    {
        var reservation = await primeDbContext.Reservations.FindAsync(id);
        if (reservation == null)
        {
            return NotFound();
        }

        await primeDbContext.Entry(reservation).Collection(r => r.Profiles).LoadAsync();
        await primeDbContext.Entry(reservation).Reference(r => r.Room).LoadAsync();

        return Ok(reservation);
    }

    [HttpPost]
    public async Task<IActionResult> Post([FromBody] NewReservation newReservation)
    {
        var room = await primeDbContext.Rooms.FirstOrDefaultAsync(r => r.Id == newReservation.RoomId);
        var guests = await primeDbContext.Profiles.Where(p => newReservation.GuestIds.Contains(p.Id)).ToListAsync();

        if (room == null || guests.Count != newReservation.GuestIds.Count)
        {
            return NotFound();
        }

        var reservation = new Reservation
        {
            Created = DateTime.UtcNow,
            From = newReservation.From.Value,
            To = newReservation.To.Value,
            Room = room,
            Profiles = guests
        };

        var createdReservation = await primeDbContext.Reservations.AddAsync(reservation);
        await primeDbContext.SaveChangesAsync();

        return Ok(createdReservation.Entity.Id);
    }
}

I named the test class ReservationControllerTests, which is the name of the class and the Tests ending at the end. In these tests, I will focus on checking how to replace data in Entity Framework Core, and not to test all possible cases.

The basis here is the appropriate preparation of PrimeDbContext for testing. The very base of the class with tests looks like this:

public class ReservationsControllerTests
{
    private DbContextOptions<PrimeDbContext> dbContextOptions = new DbContextOptionsBuilder<PrimeDbContext>()
        .UseInMemoryDatabase(databaseName: "PrimeDb")
        .Options;
    private ReservationsController controller;

    [OneTimeSetUp]
    public void Setup()
    {
        SeedDb();

        controller = new ReservationsController(new PrimeDbContext(dbContextOptions));
    }

    private void SeedDb()
    {
        using var context = new PrimeDbContext(dbContextOptions);
        var rooms = new List<Room>
        {
            new Room { Id = 1, Description = "Room nr 1", Number = 1, Level = 1, RoomType = RoomType.Standard },
            new Room { Id = 2, Description = "Room nr 2", Number = 2, Level = 1, RoomType = RoomType.Standard },
            new Room { Id = 3, Description = "Room nr 3", Number = 3, Level = 2, RoomType = RoomType.Suite }
        };

        var profiles = new List<Profile>
        {
            new Profile { Id = 1, Ref = "Profile 1", Forename = "Michał", Surname = "Białecki" },
            new Profile { Id = 2, Ref = "Profile 2", Forename = "John", Surname = "Show" },
            new Profile { Id = 3, Ref = "Profile 3", Forename = "Daenerys", Surname = "Targaryen" }
        };

        context.AddRange(rooms);
        context.AddRange(profiles);

        context.AddRange(new List<Reservation>
        {
            new Reservation
            { 
                Id = 1,
                Room = rooms[0],
                Profiles = new List<Profile>{ profiles[0] },
                From = DateTime.Today,
                To = DateTime.Today.AddDays(2)
            },
            new Reservation
            {
                Id = 2,
                Room = rooms[2],
                Profiles = new List<Profile>{ profiles[1], profiles[2] },
                From = DateTime.Today.AddDays(1),
                To = DateTime.Today.AddDays(3)
            }
        });

        context.SaveChanges();
    }
}

The first thing that immediately catches our attention is the SeedDb method, which is used to add test data to the EF Core context. For these tests, the data will be entered only once, at the very beginning thanks to the [OneTimeSetUp] attribute. The state of the database will be preserved as long as the process that performs these tests is running. However, the more important part is at the top, which is creating a dbContextOptions. Note that this is where we use the UseInMemoryDatabase option, and then create the PrimeDbContext class using this object. When creating, we give the name of the database and always use the same one. Another very important line is:

using var context = new PrimeDbContext(dbContextOptions);

At first, we use the using keyword because we don’t want Garbage Collector to remove the context variable from memory while the test is running.

Since we already have a configured database and data, it’s time to test:

[Test]
public async Task Get_FetchesReservationsWithoutRoomsAndGuests()
{
    using var context = new PrimeDbContext(dbContextOptions);
    var reservations = (await controller.Get()).ToList();

    reservations.Count.Should().Be(2);
    reservations.All(r => r.Room == null).Should().BeFalse();
    reservations.All(r => r.Profiles == null).Should().BeTrue();
}

In the first test, we get all reservations and check if their dependencies are loaded. In this case, it won’t, because the Get method in the controller doesn’t force dependencies to be loaded. Let’s check another method.

[Test]
public async Task GetById_WhenIdIsProvided_FetchesReservationWithRoomsAndGuests()
{
    using var context = new PrimeDbContext(dbContextOptions);
    var result = await controller.GetById(2);
    var okResult = result.As<OkObjectResult>();
    var reservation = okResult.Value.As<Reservation>();

    reservation.Should().NotBeNull();
    reservation.Profiles.Should().NotBeNull();
    reservation.Room.Should().NotBeNull();
}

In the second test, we take a single booking and here we check that both the room and the profiles are loaded. This is because in the GetById method we use the Collection and Reference methods to load these dependencies. Now let’s test the Post method.

[Test]
public async Task Post_WithRoomAndProfiles_AddsReservation()
{
    var newReservation = new NewReservation
    {
        From = DateTime.Today.AddDays(3),
        To = DateTime.Today.AddDays(7),
        RoomId = 3,
        GuestIds = new List<int> { 2 }
    };

    using var context = new PrimeDbContext(dbContextOptions);
    var result = await controller.Post(newReservation);

    var okResult = result.As<OkObjectResult>();
    var reservationId = okResult.Value.As<int>();
    var addedReservation = await context.Reservations
        .Include(p => p.Profiles)
        .Include(r => r.Room)
        .FirstOrDefaultAsync(r => r.Id == reservationId);

    addedReservation.Should().NotBeNull();
    addedReservation.Profiles.Should().NotBeNull();
    addedReservation.Profiles.Count.Should().Be(1);
    addedReservation.Profiles[0].Id.Should().Be(2);
    addedReservation.Room.Should().NotBeNull();
    addedReservation.Room.Id.Should().Be(3);
}

The last test checks if the added reservation was added correctly. We check whether the room and the guest’s profile have been properly assigned to the new booking.

The summary

Unit testing in Entity Framework Core is really simple and understandable. Only a few lines of configuration allow us to use the dbContext class to prepare the desired database state. We do not have to replace individual collections in PrimeDbContext separately, as was the case with the Entity Framework tests. In this respect, Entity Framework Core is refined, and unit testing using it does not differ significantly from any other unit tests. Working with them is easy and fun, which is exactly as it should be.

All code mentioned here is available on my GitHub, feel free to take a look at how EF Core can be used in other parts of the project.

Thanks for reading! 😊