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:
- Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. Oba powinny zależeć od abstrakcji (interfejsów lub klas abstrakcyjnych).
- 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 kontenerMicrosoft.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:
- Wstrzykiwanie przez właściwości (Property Injection) oraz metody (Method Injection).
- Rejestrację nazwaną i kluczowaną z obsługą metadanych (
Named,Keyed,Meta<T>).- Zaawansowane zasięgi cyklu życia (np.
InstancePerMatchingLifetimeScopedla konkretnych transakcji/sesji).- Modułową architekturę rejestracji (
Module) i deklaratywną konfigurację w plikach JSON.- Zaawansowane przechwytywanie wywołań metod (AOP Interception bez zmian w kodzie źródłowym).
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?
- 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. - 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()interfejsuIDisposable(lubDisposeAsync()zIAsyncDisposable).
- 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. - Wybór oficjalnych zespołów .NET:
Sam zespół platformy .NET w firmie Microsoft zmigrował repozytoriadotnet/runtimeorazdotnet/aspnetcorez 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:
- Łatwo o pomyłkę w kolejności argumentów (wartość oczekiwana vs wartość rzeczywista).
- Komunikaty błędów w przypadku niepowodzenia są suche i nie mówią wiele o intencji biznesowej.
- Złożone porównania kolekcji lub zagnieżdżonych struktur obiektowych
wymagają pisania pętli i wielu instrukcji
Assert.
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:
- Zgodność z językiem naturalnym (BDD – Behavior Driven Development): Test czyta się jak specyfikację techniczną.
- 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 metodEquals(). - Weryfikacja wyjątków:
Action act = () => service.Process(-1); act.Should().Throw<ArgumentException>() .WithMessage("*wartość ujemna*"); - 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:
- Etap konfiguracji: Wykorzystujemy obiekt
ContainerBuilderdo rejestracji typów, interfejsów, parametrów i zasad czasu życia. - Etap wykonania: Wywołujemy metodę
builder.Build(), uzyskując niemutowalny obiekt konteneraIContainer, 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:
InstancePerDependency()(Domyślny / Transient):
Za każdym razem, gdy kontener rozwiązuje daną zależność, tworzy całkowicie nowy obiekt. Brak współdzielenia instancji.SingleInstance()(Singleton):
Jedna, unikalna instancja na cały cykl życia głównego kontenera aplikacji (IContainer). Każde zapytanie otrzymuje dokładnie ten sam obiekt.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ę.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ę:
- Jeśli komponent został zarejestrowany jako
SingleInstance(), jego metodaDispose()zostanie wywołana dopiero w momencie zwalniania głównego kontenera aplikacji (container.Dispose()). - Jeśli komponent został zarejestrowany jako
InstancePerLifetimeScope(), jego metodaDispose()zostanie wywołana automatycznie w momencie opuszczenia blokuusing (var scope = container.BeginLifetimeScope()). - Pułapka wycieku pamięci (Captive Dependencies):
Jeśli obiekt typu Singleton (
SingleInstance) przyjmie w konstruktorze zależność o czasie życia Scoped lub Transient, zależność ta zostanie uwięziona na stercie tak długo, jak żyje Singleton, co uniemożliwi jej zwolnienie przez Garbage Collector.
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.
- Zalety: Klasa jest zawsze w stanie spójnym;
zależności są jawne; obiekt może być niemutowalny
(
readonly). - Wsparcie C# 12+: Doskonale współpracuje z
konstruktorami pierwotnymi klas (Primary Constructors):
public class Worker(ILogger logger) { public void Work() => logger.Log("Wykonuję zadanie..."); }
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).
- Realizowane w Autofac za pomocą metod
.PropertiesAutowired()lub.WithProperty(). - Wada: Zależności są ukryte; istnieje ryzyko
wywołania metody na obiekcie z niezainicjalizowaną właściwością
(
NullReferenceException).
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.
- Realizowane w Autofac poprzez zdarzenie cyklu życia
.OnActivated(e => e.Instance.SetDependency(...)).
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
- Antywzorzec Service Locator:
Przekazywanie instancjiIContainerlubIServiceProviderbezpośrednio do wnętrza klas biznesowych i wywoływaniecontainer.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. - ContainerBuilder służy wyłącznie do
konfiguracji:
Nie wolno próbować rozwiązywać zależności z poziomuContainerBuilder. Należy najpierw wywołaćbuilder.Build(). - Zasada "Ostatnia rejestracja wygrywa"
(Last-In-Wins):
Jeśli zarejestrujesz dwie różne klasy pod tym samym interfejsem bez użycia nazw/kluczy, wywołaniecontainer.Resolve<IService>()zwróci implementację zarejestrowaną jako ostatnia. Aby pobrać wszystkie implementacje, należy zażądać kolekcjiIEnumerable<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)
- Utwórz za pomocą .NET CLI nowe rozwiązanie o nazwie
LabIoC:dotnet new sln -n LabIoC - 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 - W projekcie
LabIoC.Corezdefiniuj interfejsILoggeroraz 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}"); } - Zdefiniuj klasę
Worker, przyjmującąILoggerprzez 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 } } - Zbuduj solucję i zweryfikuj poprawność kompilacji:
dotnet build
Część 2: Testy Jednostkowe w xUnit z Użyciem FluentAssertions (20 pkt)
- 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 - Dodaj referencję z projektu testowego do projektu
LabIoC.Core:dotnet add LabIoC.UnitTests/LabIoC.UnitTests.csproj reference LabIoC.Core/LabIoC.Core.csproj - Zainstaluj w projekcie testowym pakiet
FluentAssertions:
dotnet add LabIoC.UnitTests/LabIoC.UnitTests.csproj package FluentAssertions - 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); } - 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
ArgumentExceptionprzy ujemnym argumencie z wykorzystaniem asercjiact.Should().Throw<ArgumentException>(). - Test parametryzowany teorią (
[Theory]z atrybutami[InlineData]): Weryfikujący obliczenia dla wartości0,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); } } - Test faktu (
- Uruchom testy z poziomu wiersza poleceń:
dotnet test
Część 3: Instalacja i Podstawowa Konfiguracja Kontenera Autofac (15 pkt)
- 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 - Zainstaluj w projekcie
LabIoC.Apppakiet Autofac:dotnet add LabIoC.App/LabIoC.App.csproj package Autofac - W pliku
Program.csskonfiguruj kontener imperatywnie:- Zarejestruj
Loggerjako usługę udostępniającą interfejsILogger. - Zarejestruj klasę
Worker. - Zbuduj kontener za pomocą
builder.Build(). - Rozwiąż instancję
Workeri 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); - Zarejestruj
- 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)
- Dodaj do projektu
LabIoC.CoreklasęNumLoggero 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"); } } - 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"); } - Zarejestruj w kontenerze:
NumLoggerpod unikalną nazwą"numlog"jako Singleton ze wskazanym parametrem początkowyminitialValue = 100:builder.RegisterType<NumLogger>() .Named<ILogger>("numlog") .WithParameter("initialValue", 100) .SingleInstance();PropertyWorkerz wstrzyknięciem właściwości poprzez zdarzenie.OnActivated:builder.RegisterType<PropertyWorker>() .OnActivated(e => e.Instance.Logger = e.Context.ResolveNamed<ILogger>("numlog"));MethodWorkerze wstrzyknięciem przez wywołanie metodySetLoggerw zdarzeniu.OnActivated:builder.RegisterType<MethodWorker>() .OnActivated(e => e.Instance.SetLogger(e.Context.ResolveNamed<ILogger>("numlog")));
- 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)
- W projekcie
LabIoC.Corezdefiniuj 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(); } - Zarejestruj
UnitOfWorkze strategią czasu życiaInstancePerLifetimeScope():builder.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerLifetimeScope(); - W programie utwórz dwa odrębne zasięgi
using (var scope1 = container.BeginLifetimeScope()):- Wewnątrz
scope1rozwiąż dwukrotnie usługęIUnitOfWorki udowodnij, że identyfikatoryIdsą identyczne (ta sama instancja w granicach zasięgu). - Wewnątrz
scope2rozwiąż usługęIUnitOfWorki udowodnij, że identyfikator różni się od instancji zescope1.
- Wewnątrz
- 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)
- W projekcie
LabIoC.Corezaimplementuj 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; } - Zarejestruj
TransactionContextjako współdzielony wyłącznie wewnątrz zasięgu oznaczonym etykietą"transaction":builder.RegisterType<TransactionContext>() .As<ITransactionContext>() .InstancePerMatchingLifetimeScope("transaction"); builder.RegisterType<StepService>(); - Zweryfikuj, że próba rozwiązania
ITransactionContextbezpośrednio z głównego kontenera (container.Resolve<ITransactionContext>()) rzuca wyjątekDependencyResolutionException. - Otwórz zasięg z tagiem
using var txScope = container.BeginLifetimeScope("transaction"), rozwiążStepServicei wyświetl identyfikator transakcji.
Część 7: Konfiguracja Deklaratywna w Pliku JSON (10 pkt)
- Zainstaluj w
LabIoC.Apppakiety 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 - Dodaj plik
appsettings.jsondo projektuLabIoC.App:{ "autofac": { "defaultAssembly": "LabIoC.Core", "components": [ { "type": "LabIoC.Core.Logger", "services": [ { "type": "LabIoC.Core.ILogger" } ], "instanceScope": "single-instance" } ] } } - W pliku
LabIoC.App.csprojupewnij się, że plik JSON jest kopiowany do katalogu wyjściowego:<ItemGroup> <None Update="appsettings.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup> - 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:
- 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)?
- 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?
- Czym różni się czas życia
SingleInstance()odInstancePerLifetimeScope()i w jakim scenariuszu ich pomieszanie może doprowadzić do wycieku pamięci (Captive Dependency)? - Dlaczego antywzorzec Service Locator (przekazywanie
kontenera
IContainerdo klas biznesowych) jest szkodliwy dla architektury aplikacji? - W jaki sposób FluentAssertions ułatwia weryfikację złożonych
obiektów w porównaniu do standardowego
Assert.Equal()?