Języki programowania na platformie .NET / Programowanie .NET

Rok akademicki: 2026/27

Instrukcja laboratoryjna nr 2: Testy Jednostkowe (xUnit, FluentAssertions) oraz Kontener DI / IoC (Autofac)

Prowadzący: Tomasz Goluch
Wersja: 1.0


1. Wprowadzenie

Cel: Zrozumienie paradygmatu Odwrócenia Sterowania (IoC), Zasady Odwrócenia Zależności (DIP z zasad S.O.L.I.D.), mechanizmu Wstrzykiwania Zależności (DI), a także roli automatycznych testów jednostkowych w projektowaniu modularnego i łatwego w utrzymaniu kodu na platformie .NET.

W tradycyjnym podejściu imperatywnym obiekty wyższego poziomu samodzielnie tworzą i zarządzają cyklem życia obiektów niższego poziomu za pomocą operatora new. Prowadzi to do silnego sprzężenia (tight coupling), uniemożliwia testowanie w izolacji oraz wymusza kaskadowe zmiany w kodzie przy modyfikacji implementacji zależności.

Zasada Dependency Inversion Principle (DIP) głosi, że:

  1. Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. Oba powinny zależeć od abstrakcji (interfejsów lub klas abstrakcyjnych).
  2. Abstrakcje nie powinny zależeć od szczegółów implementacyjnych. To szczegóły powinny zależeć od abstrakcji.

Mechanizm Wstrzykiwania Zależności (Dependency Injection – DI) to technika realizacji wzorca Odwrócenia Sterowania (IoC – Inversion of Control), w której odpowiedzialność za dostarczenie instancji zależności zostaje przeniesiona z wnętrza klasy na zewnętrzny podmiot – programistę lub wyspecjalizowany Kontener IoC.

Tradycyjne sprzężenie (Bez DI):
[ Klasa Klienta ] ─── new Logger() ───► [ Konkretna implementacja Logger ]

Odwrócenie sterowania (IoC / DI):
[ Klasa Klienta ] ─── zależy od ──────► [ Interfejs ILogger ]
                                               ▲
                                               │ implementuje
[ Kontener IoC ] ─── wstrzykuje ──────► [ Konkretny Logger ]

🧠 Zrozumieć różnicę: Kontener wbudowany (.NET DI) vs Zaawansowane kontenery (Autofac)
Środowisko .NET od wersji Core dostarcza wbudowany, minimalistyczny kontener Microsoft.Extensions.DependencyInjection (IServiceCollection / IServiceProvider). Jest on lekki i zoptymalizowany pod kątem standardowych aplikacji ASP.NET Core.
Zaawansowane kontenery, takie jak Autofac, oferują funkcjonalności wykraczające poza standard:


2. Ekosystem Testów Jednostkowych w .NET: MSTest vs NUnit vs xUnit

Cel: Zrozumienie ewolucji frameworków testowych na platformie .NET oraz poznanie powodów, dla których xUnit stał się standardem branżowym.

W ekosystemie .NET historycznie ukształtowały się trzy dominujące frameworki do tworzenia testów jednostkowych:

Cecha MSTest (v1/v2/v3) NUnit (v3/v4) xUnit.net (v2/v3)
Pochodzenie Narzędzie Microsoftu, silnie powiązane z IDE Visual Studio Port biblioteki JUnit z Javy do środowiska .NET Zaprojektowany od podstaw dla nowoczesnego .NET przez twórców NUnit
Izolacja testów Zmienna (jedna instancja na klasę lub per test) Współdzielona instancja klasy dla wszystkich testów Pełna izolacja: Nowa instancja klasy testowej dla każdego pojedynczego testu
Inicjalizacja i Sprzątanie Atrybuty [TestInitialize], [TestCleanup] Atrybuty [SetUp], [TearDown] Standardowy język C#: Konstruktor oraz interfejs IDisposable
Oznaczanie testu podstawowego [TestMethod] [Test] [Fact]
Testy parametryzowane [DataTestMethod], [DataRow] [TestCase], [TestCaseSource] [Theory], [InlineData], [MemberData]
Współdzielenie stanu [ClassInitialize], [ClassCleanup] [OneTimeSetUp], [OneTimeTearDown] Typowane mechanizmy: IClassFixture<T>, ICollectionFixture<T>
Wielowątkowość (Równoległość) Opcjonalna, konfigurowana atrybutami Opcjonalna, domyślnie sekwencyjna Włączona domyślnie (klasy testowe uruchamiane w odrębnych wątkach)

Dlaczego xUnit to najlepszy wybór w nowoczesnym .NET?

  1. Niezrównana izolacja stanu (Zero Test Pollution):
    W NUnit klasa testowa jest instancjonowana tylko raz dla wszystkich metod testowych w niej zawartych. Jeśli w jednym teście zmienisz stan pól klasy, zmiana ta wpłynie na kolejne testy, prowadząc do tzw. flaky tests (losowo zawodzących testów).
    xUnit tworzy całkowicie nowy obiekt klasy testowej przed wywołaniem każdej metody [Fact]. Stan jest zawsze czysty.
  2. Czystość kodu zgodna z paradygmatem OOP:
    Zamiast sztucznych atrybutów [SetUp] i [TearDown], xUnit wykorzystuje naturalne mechanizmy C#:
    • Przygotowanie środowiska testowego odbywa się w konstruktorze.
    • Sprzątanie zasobów realizuje metoda Dispose() interfejsu IDisposable (lub DisposeAsync() z IAsyncDisposable).
  3. Maksymalna wydajność dzięki asynchroniczności i paralelizacji:
    xUnit od początku projektowano z myślą o środowiskach CI/CD. Testy z różnych klas uruchamiane są równolegle w osobnych wątkach puli, co skraca czas trwania całego potoku budowania.
  4. Wybór oficjalnych zespołów .NET:
    Sam zespół platformy .NET w firmie Microsoft zmigrował repozytoria dotnet/runtime oraz dotnet/aspnetcore z MSTest/NUnit właśnie do xUnit.

3. Płynne Asercje z Biblioteką FluentAssertions

Cel: Opanowanie deklaratywnego i czytelnego stylu weryfikacji wyników testów za pomocą biblioteki FluentAssertions.

Standardowe metody asercji (np. Assert.Equal(expected, actual) w xUnit) posiadają zasadnicze wady:

Biblioteka FluentAssertions (oraz jej minimalistyczna alternatywa Shouldly) rozwiązuje te problemy, wprowadzając płynne API (Fluent Interface) oparte na metodzie rozszerzającej .Should().

// Podejście klasyczne (xUnit Assert):
Assert.NotNull(result);
Assert.Equal(42, result.Id);
Assert.True(result.Items.Count > 0);

// Podejście z FluentAssertions:
result.Should().NotBeNull();
result.Id.Should().Be(42);
result.Items.Should().NotBeEmpty()
      .And.HaveCountGreaterThan(0)
      .And.Contain(x => x.Name == "DotNet");

Kluczowe zalety FluentAssertions:

  1. Zgodność z językiem naturalnym (BDD – Behavior Driven Development): Test czyta się jak specyfikację techniczną.
  2. Porównywanie grafów obiektów (BeEquivalentTo): Porównuje właściwości dwóch obiektów rekurencyjnie po ich wartościach, a nie referencjach, eliminując konieczność pisania własnych metod Equals().
  3. Weryfikacja wyjątków:
    Action act = () => service.Process(-1);
    act.Should().Throw<ArgumentException>()
       .WithMessage("*wartość ujemna*");
  4. Intuicyjna obsługa upływu czasu i asynchroniczności:
    Func<Task> asyncAct = async () => await service.ExecuteLongTaskAsync();
    await asyncAct.Should().CompleteWithinAsync(2.Seconds());

4. Architektura Kontenera Autofac: Czas Życia i Zasięgi (Lifetime Scopes)

Cel: Poznanie mechaniki działania kontenera Autofac oraz precyzyjne sterowanie cyklem życia instancji komponentów.

Praca z Autofac składa się z dwóch etapów:

  1. Etap konfiguracji: Wykorzystujemy obiekt ContainerBuilder do rejestracji typów, interfejsów, parametrów i zasad czasu życia.
  2. Etap wykonania: Wywołujemy metodę builder.Build(), uzyskując niemutowalny obiekt kontenera IContainer, z którego rozwiązujemy zależności (Resolve).
[ ContainerBuilder ] ── (Rejestracja typów, czasów życia)
         │
         ▼ .Build()
   [ IContainer ] 
         ├── .Resolve<T>() (Korzeń aplikacji / Singletony)
         │
         ▼ .BeginLifetimeScope()
 [ ILifetimeScope ] ── (Izolowany kontekst: Zapytanie HTTP / Transakcja)

Strategie czasu życia (Instance Lifetimes) w Autofac:

  1. InstancePerDependency() (Domyślny / Transient):
    Za każdym razem, gdy kontener rozwiązuje daną zależność, tworzy całkowicie nowy obiekt. Brak współdzielenia instancji.
  2. SingleInstance() (Singleton):
    Jedna, unikalna instancja na cały cykl życia głównego kontenera aplikacji (IContainer). Każde zapytanie otrzymuje dokładnie ten sam obiekt.
  3. InstancePerLifetimeScope() (Scoped):
    Jedna instancja w granicach danego zasięgu (ILifetimeScope). Fundamentalna strategia w aplikacjach wielowątkowych i webowych (odpowiada pojedynczemu żądaniu HTTP). Obiekty w ramach jednego zasięgu współdzielą instancję, lecz nowy zasięg otrzymuje nową instancję.
  4. InstancePerMatchingLifetimeScope(object tag) (Tagged Scopes):
    Tworzy instancję współdzieloną wyłącznie wewnątrz zasięgu oznaczonego konkretną nazwą (tagiem). Pozwala na izolację kontekstu długotrwałych procesów, transakcji biznesowych lub sesji użytkownika.
💡 Anatomia zasięgu i zwalnianie zasobów (IDisposable) w Autofac (Kliknij, aby rozwinąć/zwinąć)

Kontenery IoC biorą na siebie nie tylko tworzenie obiektów, ale także ich sprzątanie.
Gdy komponent implementuje interfejs IDisposable lub IAsyncDisposable, kontener przejmuje nad nim kontrolę:


5. Metody Wstrzykiwania Zależności

Cel: Poznanie technik dostarczania zależności do klas oraz kryteriów ich doboru.

1. Wstrzykiwanie przez konstruktor (Constructor Injection) – Zalecany standard

Zależności deklarowane są jako parametry publicznego konstruktora.

2. Wstrzykiwanie przez właściwości (Property Injection)

Stosowane, gdy zależność jest opcjonalna lub gdy framework nie pozwala na bezpośrednią kontrolę nad konstruktorem (np. atrybuty filtrów, kontrolki GUI).

3. Wstrzykiwanie przez metodę (Method Injection)

Stosowane, gdy zależność potrzebna jest wyłącznie do wykonania jednej, specyficznej operacji i nie ma sensu utrzymywać jej jako stałego stanu klasy.


6. Konfiguracja Deklaratywna (JSON) a Imperatywna (C#)

Cel: Separacja konfiguracji komponentów od kodu źródłowego za pomocą plików JSON i modułów Autofac.

Choć konfiguracja imperatywna w kodzie C# zapewnia bezpieczeństwo typów (Type Safety) i podpowiadanie składni, konfiguracja deklaratywna w pliku appsettings.json pozwala na podmianę implementacji usług na serwerze produkcyjnym bez konieczności ponownej kompilacji aplikacji.

Do obsługi konfiguracji JSON w Autofac niezbędny jest pakiet Autofac.Configuration.

Wzorcowa struktura sekcji "autofac" w pliku appsettings.json:

{
  "autofac": {
    "defaultAssembly": "MyApplication",
    "components": [
      {
        "type": "MyApplication.Services.FileLogger",
        "services": [
          { "type": "MyApplication.Services.ILogger" }
        ],
        "instanceScope": "single-instance"
      }
    ]
  }
}

Ładowanie konfiguracji w kodzie aplikacji:

var configBuilder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);

IConfigurationRoot config = configBuilder.Build();

var builder = new ContainerBuilder();
builder.RegisterModule(new ConfigurationModule(config.GetSection("autofac")));

7. Dobre Praktyki i Antywzorce w Pracy z Kontenerem IoC

  1. Antywzorzec Service Locator:
    Przekazywanie instancji IContainer lub IServiceProvider bezpośrednio do wnętrza klas biznesowych i wywoływanie container.Resolve<T>() w metodach jest poważnym błędem architektonicznym. Ukrywa to rzeczywiste zależności klasy i wiąże kod domenowy z konkretną biblioteką kontenera.
  2. ContainerBuilder służy wyłącznie do konfiguracji:
    Nie wolno próbować rozwiązywać zależności z poziomu ContainerBuilder. Należy najpierw wywołać builder.Build().
  3. Zasada "Ostatnia rejestracja wygrywa" (Last-In-Wins):
    Jeśli zarejestrujesz dwie różne klasy pod tym samym interfejsem bez użycia nazw/kluczy, wywołanie container.Resolve<IService>() zwróci implementację zarejestrowaną jako ostatnia. Aby pobrać wszystkie implementacje, należy zażądać kolekcji IEnumerable<IService>.

Zadanie Laboratoryjne do wykonania (100 pkt)

Środowisko: Ćwiczenie należy realizować przy użyciu platformy .NET 8 lub .NET 9 z wykorzystaniem wiersza poleceń .NET CLI (dotnet) oraz edytora Visual Studio 2022 / VS Code / JetBrains Rider.


Część 1: Architektura Solucji i Aplikacja Domenowa (10 pkt)

  1. Utwórz za pomocą .NET CLI nowe rozwiązanie o nazwie LabIoC:
    dotnet new sln -n LabIoC
  2. Utwórz projekt biblioteki klas o nazwie LabIoC.Core:
    dotnet new classlib -n LabIoC.Core
    dotnet sln LabIoC.sln add LabIoC.Core/LabIoC.Core.csproj
  3. W projekcie LabIoC.Core zdefiniuj interfejs ILogger oraz klasę Logger:
    namespace LabIoC.Core;
    
    public interface ILogger
    {
        void Log(string message);
    }
    
    public class Logger : ILogger
    {
        public void Log(string message) => Console.WriteLine($"[Logger]: {message}");
    }
  4. Zdefiniuj klasę Worker, przyjmującą ILogger przez konstruktor i implementującą metodę Execute():
    namespace LabIoC.Core;
    
    public class Worker
    {
        private readonly ILogger _logger;
    
        public Worker(ILogger logger)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }
    
        public int Execute(int tasksCount)
        {
            if (tasksCount < 0)
                throw new ArgumentException("Liczba zadań nie może być ujemna.", nameof(tasksCount));
    
            _logger.Log($"Rozpoczęto przetwarzanie {tasksCount} zadań.");
            _logger.Log("Zakończono pomyślnie.");
            return tasksCount * 10; // Zwraca punkty za wykonane zadania
        }
    }
  5. Zbuduj solucję i zweryfikuj poprawność kompilacji:
    dotnet build

Część 2: Testy Jednostkowe w xUnit z Użyciem FluentAssertions (20 pkt)

  1. Utwórz w solucji projekt testowy oparty na szablonie xUnit:
    dotnet new xunit -n LabIoC.UnitTests
    dotnet sln LabIoC.sln add LabIoC.UnitTests/LabIoC.UnitTests.csproj
  2. Dodaj referencję z projektu testowego do projektu LabIoC.Core:
    dotnet add LabIoC.UnitTests/LabIoC.UnitTests.csproj reference LabIoC.Core/LabIoC.Core.csproj
  3. Zainstaluj w projekcie testowym pakiet FluentAssertions:
    dotnet add LabIoC.UnitTests/LabIoC.UnitTests.csproj package FluentAssertions
  4. Utwórz w projekcie testowym atrapę szpiegującą (Test Spy) dla interfejsu ILogger, zbierającą komunikaty do listy w pamięci:
    namespace LabIoC.UnitTests.Fakes;
    
    using LabIoC.Core;
    
    public class SpyLogger : ILogger
    {
        public List<string> Messages { get; } = new();
    
        public void Log(string message) => Messages.Add(message);
    }
  5. Utwórz klasę testową WorkerTests.cs. Zaimplementuj w niej:
    • Test faktu ([Fact]): Sprawdzający, czy poprawny argument zwraca oczekiwany wynik oraz czy logger zarejestrował wymagane wpisy (używając FluentAssertions: .Should().Be(), .Should().Contain(), .Should().HaveCount()).
    • Test wyjątku: Weryfikujący rzucenie wyjątku ArgumentException przy ujemnym argumencie z wykorzystaniem asercji act.Should().Throw<ArgumentException>().
    • Test parametryzowany teorią ([Theory] z atrybutami [InlineData]): Weryfikujący obliczenia dla wartości 0, 5, 10.
    namespace LabIoC.UnitTests;
    
    using FluentAssertions;
    using LabIoC.Core;
    using LabIoC.UnitTests.Fakes;
    using Xunit;
    
    public class WorkerTests
    {
        [Fact]
        public void Execute_ValidCount_ShouldReturnExpectedResultAndLogMessages()
        {
            // Arrange
            var spy = new SpyLogger();
            var worker = new Worker(spy);
    
            // Act
            int result = worker.Execute(3);
    
            // Assert (FluentAssertions)
            result.Should().Be(30);
            spy.Messages.Should().HaveCount(2)
                .And.Contain("Zakończono pomyślnie.");
        }
    
        [Fact]
        public void Execute_NegativeCount_ShouldThrowArgumentException()
        {
            // Arrange
            var spy = new SpyLogger();
            var worker = new Worker(spy);
    
            // Act
            Action act = () => worker.Execute(-1);
    
            // Assert
            act.Should().Throw<ArgumentException>()
               .WithMessage("*nie może być ujemna*");
        }
    
        [Theory]
        [InlineData(0, 0)]
        [InlineData(5, 50)]
        [InlineData(10, 100)]
        public void Execute_VariousCounts_ShouldCalculateScoreProperly(int input, int expected)
        {
            var spy = new SpyLogger();
            var worker = new Worker(spy);
    
            int result = worker.Execute(input);
    
            result.Should().Be(expected);
        }
    }
  6. Uruchom testy z poziomu wiersza poleceń:
    dotnet test

Część 3: Instalacja i Podstawowa Konfiguracja Kontenera Autofac (15 pkt)

  1. Utwórz projekt aplikacji konsolowej LabIoC.App:
    dotnet new console -n LabIoC.App
    dotnet sln LabIoC.sln add LabIoC.App/LabIoC.App.csproj
    dotnet add LabIoC.App/LabIoC.App.csproj reference LabIoC.Core/LabIoC.Core.csproj
  2. Zainstaluj w projekcie LabIoC.App pakiet Autofac:
    dotnet add LabIoC.App/LabIoC.App.csproj package Autofac
  3. W pliku Program.cs skonfiguruj kontener imperatywnie:
    • Zarejestruj Logger jako usługę udostępniającą interfejs ILogger.
    • Zarejestruj klasę Worker.
    • Zbuduj kontener za pomocą builder.Build().
    • Rozwiąż instancję Worker i wywołaj metodę Execute(2).
    using Autofac;
    using LabIoC.Core;
    
    var builder = new ContainerBuilder();
    
    // Rejestracja typu jako interfejs
    builder.RegisterType<Logger>().As<ILogger>();
    builder.RegisterType<Worker>();
    
    using var container = builder.Build();
    
    // Rozwiązanie zależności z kontenera
    var worker = container.Resolve<Worker>();
    worker.Execute(2);
  4. Uruchom aplikację i sprawdź wynik w konsoli:
    dotnet run --project LabIoC.App

Część 4: Strategie Wstrzykiwania Zależności (Właściwość i Metoda) (15 pkt)

  1. Dodaj do projektu LabIoC.Core klasę NumLogger o zliczającym się identyfikatorze oraz klasę PropertyWorker:
    namespace LabIoC.Core;
    
    public class NumLogger : ILogger
    {
        private int _counter;
        public NumLogger(int initialValue = 0) => _counter = initialValue;
        public void Log(string message) => Console.WriteLine($"[NumLogger ({_counter++})]: {message}");
    }
    
    public class PropertyWorker
    {
        public ILogger? Logger { get; set; }
    
        public void Work()
        {
            Logger?.Log("Praca wykonana przez PropertyWorker");
        }
    }
  2. Dodaj klasę MethodWorker, przyjmującą logger za pośrednictwem dedykowanej metody inicjalizacyjnej:
    namespace LabIoC.Core;
    
    public class MethodWorker
    {
        private ILogger? _logger;
        public void SetLogger(ILogger logger) => _logger = logger;
        public void Work() => _logger?.Log("Praca wykonana przez MethodWorker");
    }
  3. Zarejestruj w kontenerze:
    • NumLogger pod unikalną nazwą "numlog" jako Singleton ze wskazanym parametrem początkowym initialValue = 100:
      builder.RegisterType<NumLogger>()
             .Named<ILogger>("numlog")
             .WithParameter("initialValue", 100)
             .SingleInstance();
    • PropertyWorker z wstrzyknięciem właściwości poprzez zdarzenie .OnActivated:
      builder.RegisterType<PropertyWorker>()
             .OnActivated(e => e.Instance.Logger = e.Context.ResolveNamed<ILogger>("numlog"));
    • MethodWorker ze wstrzyknięciem przez wywołanie metody SetLogger w zdarzeniu .OnActivated:
      builder.RegisterType<MethodWorker>()
             .OnActivated(e => e.Instance.SetLogger(e.Context.ResolveNamed<ILogger>("numlog")));
  4. Rozwiąż oba komponenty, wykonaj ich metody Work() i zaobserwuj inkrementację licznika w wyjściu konsoli.

Część 5: Cykle Życia i Izolacja Zasięgów (Lifetime Scopes) (15 pkt)

  1. W projekcie LabIoC.Core zdefiniuj interfejs i klasę symulującą jednostkę pracy o unikalnym identyfikatorze GUID:
    namespace LabIoC.Core;
    
    public interface IUnitOfWork
    {
        Guid Id { get; }
    }
    
    public class UnitOfWork : IUnitOfWork
    {
        public Guid Id { get; } = Guid.NewGuid();
    }
  2. Zarejestruj UnitOfWork ze strategią czasu życia InstancePerLifetimeScope():
    builder.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerLifetimeScope();
  3. W programie utwórz dwa odrębne zasięgi using (var scope1 = container.BeginLifetimeScope()):
    • Wewnątrz scope1 rozwiąż dwukrotnie usługę IUnitOfWork i udowodnij, że identyfikatory Id są identyczne (ta sama instancja w granicach zasięgu).
    • Wewnątrz scope2 rozwiąż usługę IUnitOfWork i udowodnij, że identyfikator różni się od instancji ze scope1.
  4. Napisz dedykowany test jednostkowy w projekcie LabIoC.UnitTests, który za pomocą FluentAssertions zweryfikuje tę regułę:
    [Fact]
    public void UnitOfWork_LifetimeScope_ShouldBeSharedWithinScopeAndDifferentAcrossScopes()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerLifetimeScope();
        using var container = builder.Build();
    
        using var scope1 = container.BeginLifetimeScope();
        var uow1a = scope1.Resolve<IUnitOfWork>();
        var uow1b = scope1.Resolve<IUnitOfWork>();
    
        using var scope2 = container.BeginLifetimeScope();
        var uow2 = scope2.Resolve<IUnitOfWork>();
    
        uow1a.Id.Should().Be(uow1b.Id);
        uow1a.Id.Should().NotBe(uow2.Id);
    }

Część 6: Zaawansowane Zasięgi Otagowane (InstancePerMatchingLifetimeScope) (10 pkt)

  1. W projekcie LabIoC.Core zaimplementuj klasy do symulacji transakcji biznesowej:
    namespace LabIoC.Core;
    
    public interface ITransactionContext
    {
        Guid TransactionId { get; }
    }
    
    public class TransactionContext : ITransactionContext
    {
        public Guid TransactionId { get; } = Guid.NewGuid();
    }
    
    public class StepService(ITransactionContext context)
    {
        public Guid GetCurrentTransaction() => context.TransactionId;
    }
  2. Zarejestruj TransactionContext jako współdzielony wyłącznie wewnątrz zasięgu oznaczonym etykietą "transaction":
    builder.RegisterType<TransactionContext>()
           .As<ITransactionContext>()
           .InstancePerMatchingLifetimeScope("transaction");
    
    builder.RegisterType<StepService>();
  3. Zweryfikuj, że próba rozwiązania ITransactionContext bezpośrednio z głównego kontenera (container.Resolve<ITransactionContext>()) rzuca wyjątek DependencyResolutionException.
  4. Otwórz zasięg z tagiem using var txScope = container.BeginLifetimeScope("transaction"), rozwiąż StepService i wyświetl identyfikator transakcji.

Część 7: Konfiguracja Deklaratywna w Pliku JSON (10 pkt)

  1. Zainstaluj w LabIoC.App pakiety konfiguracyjne:
    dotnet add LabIoC.App/LabIoC.App.csproj package Microsoft.Extensions.Configuration
    dotnet add LabIoC.App/LabIoC.App.csproj package Microsoft.Extensions.Configuration.Json
    dotnet add LabIoC.App/LabIoC.App.csproj package Autofac.Configuration
  2. Dodaj plik appsettings.json do projektu LabIoC.App:
    {
      "autofac": {
        "defaultAssembly": "LabIoC.Core",
        "components": [
          {
            "type": "LabIoC.Core.Logger",
            "services": [
              { "type": "LabIoC.Core.ILogger" }
            ],
            "instanceScope": "single-instance"
          }
        ]
      }
    }
  3. W pliku LabIoC.App.csproj upewnij się, że plik JSON jest kopiowany do katalogu wyjściowego:
    <ItemGroup>
      <None Update="appsettings.json">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      </None>
    </ItemGroup>
  4. Zastąp imperatywną rejestrację modułem ładującym sekcję "autofac" z pliku JSON:
    using Microsoft.Extensions.Configuration;
    using Autofac.Configuration;
    
    var config = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json")
        .Build();
    
    var builder = new ContainerBuilder();
    builder.RegisterModule(new ConfigurationModule(config.GetSection("autofac")));
    builder.RegisterType<Worker>();
    
    using var container = builder.Build();
    var worker = container.Resolve<Worker>();
    worker.Execute(1);

Zestawienie Oceny (Suma: 100 pkt)

Nr Zakres modułu Weryfikacja Punkty
1 Architektura Solucji i DIP Struktura projektów, interfejs ILogger, klasa Worker, zasada DIP. 10 pkt
2 Testy xUnit & FluentAssertions Testy [Fact], [Theory], weryfikacja wyjątków, atrapa SpyLogger. 20 pkt
3 Podstawowa rejestracja Autofac ContainerBuilder, rejestracja interfejsu, budowanie i Resolve. 15 pkt
4 Wstrzykiwanie przez Właściwość i Metodę Zdarzenie .OnActivated, rejestracja nazwana .Named(). 15 pkt
5 Cykl życia Scoped (Lifetime Scopes) Izolacja instancji w BeginLifetimeScope(), test weryfikujący xUnit. 15 pkt
6 Zasięgi Otagowane (Matching Scopes) InstancePerMatchingLifetimeScope("transaction"), izolacja transakcji. 10 pkt
7 Konfiguracja Deklaratywna JSON Plik appsettings.json, moduł ConfigurationModule, kopiowanie pliku. 15 pkt
SUMA 100 pkt

Przykładowe pytania na obronie ustnej:

  1. Czym różni się cykl życia klasy testowej w frameworku xUnit od NUnit i dlaczego rozwiązanie z xUnit zapobiega powstawaniu testów niestabilnych (flaky tests)?
  2. Dlaczego wstrzykiwanie zależności przez konstruktor jest uznawane za najlepszą praktykę, a wstrzykiwanie przez właściwości uważa się za podatne na błędy?
  3. Czym różni się czas życia SingleInstance() od InstancePerLifetimeScope() i w jakim scenariuszu ich pomieszanie może doprowadzić do wycieku pamięci (Captive Dependency)?
  4. Dlaczego antywzorzec Service Locator (przekazywanie kontenera IContainer do klas biznesowych) jest szkodliwy dla architektury aplikacji?
  5. W jaki sposób FluentAssertions ułatwia weryfikację złożonych obiektów w porównaniu do standardowego Assert.Equal()?