.NET Dependency Injection (DI)

Dependency Injection je návrhový vzor, který umožňuje odstranit pevné vazby mezi softwarovými komponentami. Místo přímého vytváření závislých objektů v rámci třídy, DI poskytuje tyto závislosti zvenčí, což vede k větší flexibilitě a oddělení. Ve světě C#, kde aplikace často rostou do velké komplexnosti a musí být snadno testovatelné a rozšiřitelné, je DI klíčovým nástrojem, který vývojářům pomáhá dosáhnout těchto cílů.

Pojďme si ukázat životní cykly DI služeb – jako jsou Singleton, Scoped a Transient.

Singleton

  • Životní cyklus: Instance služby Singleton je vytvořena pouze jednou za dobu existence aplikace (nebo hostitele). To znamená, že všechny požadavky a operace používají stejnou instanci služby.
  • Použití: Singleton je vhodný pro služby, které mají globální stav nebo konfiguraci, která se nemění, nebo pro služby, které vyžadují pouze jednu instanci (například logovací služby, přístup k konfiguračním souborům atd.).
  • Rizika: Je třeba být opatrný při používání stavových služeb Singleton ve vícevláknových aplikacích, protože to může vést k problémům s bezpečností vláken.

Scoped

  • Životní cyklus: Scoped služby jsou vytvořeny jednou pro každý scope, typicky pro každý HTTP požadavek v ASP.NET Core. Každý požadavek má svou vlastní instanci scoped služby.
  • Použití: Scoped služby jsou ideální pro operace, které jsou specifické pro konkrétní požadavek nebo transakci, jako jsou například databázové kontexty v Entity Framework.
  • Rizika: Scoped služby by neměly být sdíleny mezi různými požadavky, protože mohou obsahovat stav specifický pro konkrétní požadavek.

Transient

  • Životní cyklus: Transient služby jsou vytvářeny pokaždé, když jsou požadovány. To znamená, že každé vložení (injection) nebo každý požadavek na službu vytvoří novou instanci této služby.
  • Použití: Jsou ideální pro lehké, bezstavové služby. Protože jsou transient služby krátkodobé, jsou vhodné pro služby, které neuchovávají informace o stavu nebo zdroje mezi jednotlivými požadavky.
  • Rizika: Vytváření nové instance při každém požadavku může být náročné na zdroje, pokud je služba náročná na vytvoření, nebo pokud je často požadována.

Shrnutí:

  • Singleton: Jedna instance pro celou aplikaci.
  • Scoped: Jedna instance na scope (typicky na HTTP požadavek).
  • Transient: Nová instance při každém požadavku na službu.

Při návrhu aplikace je důležité pečlivě zvážit, který životní cyklus je pro každou službu nejvhodnější, aby se dosáhlo správného vyvážení mezi výkonem, bezpečností vláken a správou zdrojů.

.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));
        }
    }
}