.Net core + NUnit testujeme a mockujeme

Mockování je důležitým konceptem při psaní jednotkových testů z několika důvodů:

  1. Izolace od závislostí: Mockování umožňuje oddělit testovanou třídu od jejích závislostí. To znamená, že při testování konkrétní třídy nemusíte brát v potaz implementaci jejích závislostí. Místo toho můžete vytvořit mock objekty, které simulují chování těchto závislostí.
  2. Opakovatelnost a konzistence: Při použití skutečných závislostí, jako jsou databáze nebo externí API, mohou testy být nekonzistentní a záviset na stavu těchto závislostí. Použití mocků umožňuje vytvořit konzistentní a opakovatelné testy, které nejsou závislé na externích faktorech.
  3. Rychlost testů: Skutečné závislosti mohou být pomalé nebo nedostupné v určitých situacích, což může zpomalit běh testů. Použití mocků umožňuje rychlejší spouštění testů, protože mocky jsou obvykle rychlejší než skutečné závislosti.
  4. Izolace chyb: Pokud test selže, mocky vám umožňují izolovat problém na konkrétní část kódu, kterou testujete. Bez mocků by selhání mohlo být způsobeno problémem v externí závislosti, což by mohlo být složité odhalit.
  5. Testování hraničních podmínek a chybových stavů: Mocky umožňují snadno simulovat různé situace, včetně chybových stavů, které mohou být obtížné dosáhnout s reálnými závislostmi.
  6. Zvýšení rychlosti vývoje: Testy s mocky umožňují rychlejší iterace při vývoji. Můžete psát testy pro nový kód, i když jeho závislosti nejsou ještě implementovány.
  7. Snížení nákladů na infrastrukturu: Pokud byste všechny testy spouštěli s reálnými závislostmi, mohlo by to vyžadovat drahou infrastrukturu, například databázový server. Mockování umožňuje testování bez skutečné infrastruktury.

Celkově mockování je důležitou technikou pro vytváření izolovaných a efektivních jednotkových testů, které vám pomáhají zajistit kvalitu kódu a snižovat rizika chyb.

Instalujeme Moq

Abychom mohli začít mockovat doinstalujeme Nuget do projektu s testy:

Install-Package Moq

Kód pro otestování

Mějme controller a service, které chceme otestovat. Service se do controlleri injectne přes DI:

ProductController.cs:

namespace WebAPiDi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class TestController : ControllerBase
    {
        readonly IProductService _productService;

        public TestController(IProductService productService)
        {
            _productService = productService;
        }

        [HttpGet]
        public IActionResult Shop()
        {
            return Ok(_productService.Shop());
        }
    }
}

ProductService.cs:

namespace WebAPiDi.Services
{
    public class ProductService : IProductService
    {
        public IEnumerable<ProductDto> Shop()
        {
            return new List<ProductDto>()
            {
                new ProductDto() { Id = 1, Name = "Best product" }
            };
        }
    }
}

IProductService.cs:

namespace WebAPiDi.Services
{
    public interface IProductService
    {
        IEnumerable<ProductDto> Shop();
    }
}

productDto.cs:

namespace WebAPiDi.Dtos
{
    public class ProductDto
    {
        public int Id { get; set; }

        public required string Name { get; set; }
    }
}

Program.cs (konfigurace sávislostí):

builder.Services.AddScoped<IProductService, ProductService>();

Testujeme

V minulých dílech jsme si ukázali jak zprovoznit NUnit a Moq. Dnes už je jdeme rovnou používat.

Testujeme service

Začneme tím jednodušším – otestujme ProducService. Nepotřebujeme nic mockovat, pouze si uděláme instanci ProuctService a zavoláním metody Shop zkontrolujeme že vrátí 1 záznam, ve kterém zkontrolujeme vyplnění propert:

namespace Test.Services
{
    [TestFixture]
    public class ProductServiceTests
    {
        [Test]
        public void Shop_ReturnsListOfProducts()
        {
            // Arrange
            IProductService productService = new ProductService();

            // Act
            IEnumerable<ProductDto> products = productService.Shop();

            // Assert
            Assert.IsNotNull(products);
            Assert.IsInstanceOf<IEnumerable<ProductDto>>(products);
            Assert.That(products.Count(), Is.EqualTo(1));

            foreach (var product in products)
            {
                Assert.IsNotNull(product.Id);
                Assert.IsNotNull(product.Name);
            }
        }
    }
}

Při testování controlleru budeme muset použít mockování. Namockujeme se ProductService, čímž vytvoříme izovali od této service a můžeme si nasimulovat nejrůznější chování této service.

namespace Test.Controllers
{
    [TestFixture]
    public class TestControllerTests
    {
        [Test]
        public void Shop_ReturnsOkResultWithProducts()
        {
            // Arrange
            var productServiceMock = new Mock<IProductService>();
            productServiceMock.Setup(repo => repo.Shop()).Returns(new List<ProductDto>
            {
                new ProductDto() { Id = 68, Name = "Jarda Jagr" },
                new ProductDto() { Id = 88, Name = "pasta" }
            });

            var controller = new TestController(productServiceMock.Object);

            // Act
            var result = controller.Shop() as OkObjectResult;

            // Assert
            Assert.IsNotNull(result);
            Assert.That(result.StatusCode, Is.EqualTo(200));

            var products = result.Value as List<ProductDto>;
            Assert.IsNotNull(products);
            Assert.That(products.Count, Is.EqualTo(2));
        }

        [Test]
        public void Shop_ReturnsEmptyProducts()
        {
            // Arrange
            var productServiceMock = new Mock<IProductService>();
            productServiceMock.Setup(repo => repo.Shop()).Returns(new List<ProductDto>());

            var controller = new TestController(productServiceMock.Object);

            // Act
            var result = controller.Shop() as OkObjectResult;

            // Assert
            Assert.IsNotNull(result);
            Assert.That(result.StatusCode, Is.EqualTo(200));

            var products = result.Value as List<ProductDto>;
            Assert.IsNotNull(products);
            Assert.That(products.Count, Is.EqualTo(0));
        }
    }
}

.Net core + NUnit testujeme statickou třídu

Testování statických tříd je .Net velice jednoduché. Nemusíte nic mockovat a jednoduše napíšete test. NUnit umožňuje testovat statické třídy stejně jako běžné třídy, ale nemáte k dispozici instanci třídy, takže nemůžete testovat instanční metody ani vlastnosti. Místo toho se zaměříme na testování statických metod a vlastností.

public static class MathUtility
{
    public static int Add(int a, int b)
    {
        return a + b;
    }

    public static int Subtract(int a, int b)
    {
        return a - b;
    }
}

Ve Visual Studiu založíme ve stejné solution nový projekt, který bude vycházet ze šablony Nunit Test Project. Do toho projektu založíme nový test:

namespace Test.Statics
{
    [TestFixture]
    public class MathUtilityTests
    {
        [Test]
        public void Add_ShouldReturnCorrectSum()
        {
            int result = MathUtility.Add(3, 5);
            Assert.AreEqual(8, result);
        }

        [Test]
        public void Subtract_ShouldReturnCorrectDifference()
        {
            int result = MathUtility.Subtract(10, 3);
            Assert.AreEqual(7, result);
        }
    }
}

Po spuštění testu (na testovacím projektu klikneme pravým talčítkem a vyberme volbu Run tests) vidíme výsledky testů

Jak funguje Assert.AreEqual

Metoda Assert.AreEqual(8, result) v NUnit je způsob, jak otestovat, zda očekávaná hodnota (v tomto případě 8) je rovna aktuální hodnotě (v tomto případě result). Pokud jsou tyto dvě hodnoty shodné, test projde a pokračuje v běhu testů. Pokud jsou hodnoty různé, test selže a testovací běh je přerušen.

Zde je, jak to funguje:

  • Pokud result (aktuální hodnota) a 8 (očekávaná hodnota) jsou stejné, test projde a není vyvolána výjimka.
  • Pokud result a 8 nejsou stejné, test selže a vyvolá se výjimka AssertionException, která způsobí, že testovací běh se zastaví a označí test jako nevyhovující.

Tato metoda je používána k porovnání dvou hodnot a určení, zda jsou shodné. Použití této metody v testech je běžným způsobem ověření správného chování kódu. V tomto konkrétním případě se testuje, zda výstup z metody MathUtility.Add(3, 5) je roven 8.

Poznámka: Můžete také použít jiné metody Assert, jako například Assert.IsTrue, Assert.IsFalse, nebo Assert.IsNull, atd., v závislosti na tom, co chcete otestovat.

Vylepšení

Můžete si všimnout, že Visual Studio řádek s ověřením výsledku podtrhne, protože se mu na něm něco nelíbí:

Assert.AreEqual(8, result);

U řádku vidíme varování: Consider using the constraint model, Assert.That(actual, Is.EqualTo(expected)), instead of the classic model, Assert.AreEqual(expected, actual).

Více o problému se dočete zde: https://docs.nunit.org/articles/nunit-analyzers/NUnit2005.html

Pokud se chcete toto varování opravit, přepište ověření výsledku na:

Assert.That(result, Is.EqualTo(7));