unit-testing
Wstrzykiwanie zależności
Szukaj…
Uwagi
Jednym z podejść, które można zastosować do pisania oprogramowania, jest tworzenie zależności według potrzeb. Jest to dość intuicyjny sposób pisania programu i jest to sposób, w jaki większość ludzi będzie się uczyć, częściowo dlatego, że jest łatwy do naśladowania. Jednym z problemów związanych z tym podejściem jest to, że testowanie może być trudne. Rozważ metodę, która wykonuje pewne przetwarzanie w oparciu o bieżącą datę. Metoda może zawierać pewien kod, taki jak poniżej:
if (DateTime.Now.Date > processDate)
{
// Do some processing
}
Kod jest bezpośrednio zależny od bieżącej daty. Ta metoda może być trudna do przetestowania, ponieważ bieżąca data nie może być łatwo zmanipulowana. Jednym ze sposobów uczynienia kodu bardziej testowalnym jest usunięcie bezpośredniego odniesienia do bieżącej daty i zamiast tego dostarczenie (lub wstrzyknięcie) bieżącej daty do metody wykonującej przetwarzanie. Ta iniekcja zależności może znacznie ułatwić testowanie aspektów kodu przy użyciu podwójnych testów w celu uproszczenia etapu konfiguracji testu jednostkowego.
Systemy MKOl
Innym aspektem do rozważenia jest żywotność zależności; w przypadku, gdy sama klasa tworzy własne zależności (znane również jako niezmienniki), wówczas jest odpowiedzialna za ich usunięcie. Dependency Injection odwraca to (i dlatego często nazywamy bibliotekę iniekcji systemem „Inversion of Control”) i oznacza, że zamiast klasy odpowiedzialnej za tworzenie, zarządzanie i czyszczenie swoich zależności, agent zewnętrzny (w tym przypadek, system IoC) robi to zamiast tego.
To znacznie ułatwia posiadanie zależności, które są współużytkowane przez instancje tej samej klasy; na przykład rozważ usługę, która pobiera dane z punktu końcowego HTTP, aby klasa mogła je wykorzystać. Ponieważ ta usługa jest bezstanowa (tzn. Nie ma żadnego stanu wewnętrznego), dlatego naprawdę potrzebujemy tylko jednego wystąpienia tej usługi w całej naszej aplikacji. Chociaż jest to możliwe (na przykład przy użyciu klasy statycznej), aby to zrobić ręcznie, o wiele łatwiej jest utworzyć klasę i powiedzieć systemowi IoC, że ma zostać utworzony jako Singleton , przy czym każda z nich istnieje tylko jedna instancja klasy.
Innym przykładem mogą być konteksty bazy danych używane w aplikacji internetowej, w których wymagany jest nowy kontekst na żądanie (lub wątek), a nie na wystąpienie kontrolera; pozwala to wstrzykiwać kontekst do każdej warstwy wykonanej przez ten wątek, bez konieczności ręcznego przekazywania.
Dzięki temu konsumenci nie muszą zarządzać zależnościami.
Wtrysk Konstruktora
Wstrzykiwanie konstruktora jest najbezpieczniejszym sposobem wstrzykiwania zależności, od których zależy cała klasa. Takie zależności są często nazywane niezmiennikami , ponieważ nie można utworzyć instancji klasy bez ich podania. Wymagając wstrzyknięcia zależności podczas budowy, gwarantuje się, że nie można utworzyć obiektu w niespójnym stanie.
Rozważ klasę, która musi zapisywać do pliku dziennika w warunkach błędu. Jest zależny od ILogger
, który można wstrzykiwać i stosować w razie potrzeby.
public class RecordProcessor
{
readonly private ILogger _logger;
public RecordProcessor(ILogger logger)
{
_logger = logger;
}
public void DoSomeProcessing() {
// ...
_logger.Log("Complete");
}
}
Czasami podczas pisania testów można zauważyć, że konstruktor wymaga więcej zależności niż jest to faktycznie potrzebne do testowania sprawy. Im więcej takich testów masz, tym bardziej prawdopodobne jest, że Twoja klasa łamie zasadę pojedynczej odpowiedzialności (SRP). Dlatego zdefiniowanie domyślnego zachowania dla wszystkich prób wstrzykiwanych zależności na etapie inicjalizacji klasy testowej nie jest zbyt dobrą praktyką, ponieważ może maskować potencjalny sygnał ostrzegawczy.
Najbardziej nieprzystosowany do tego wyglądałby tak:
[Test]
public void RecordProcessor_DependencyInjectionExample()
{
ILogger logger = new FakeLoggerImpl(); //or create a mock by a mocking Framework
var sut = new RecordProcessor(logger); //initialize with fake impl in testcode
Assert.IsTrue(logger.HasCalledExpectedMethod());
}
Zastrzyk nieruchomości
Wstrzykiwanie właściwości umożliwia aktualizację zależności klas po jej utworzeniu. Może to być przydatne, jeśli chcesz uprościć tworzenie obiektów, ale nadal zezwalaj na zastępowanie zależności przez testy z podwójnymi testami.
Rozważ klasę, która musi zapisać do pliku dziennika w stanie błędu. Klasa wie, jak zbudować domyślny program Logger
, ale pozwala na zastąpienie go przez wstrzyknięcie właściwości. Warto jednak zauważyć, że w ten sposób za pomocą wstrzykiwania właściwości ściśle łączysz tę klasę z dokładną implementacją ILogger
którym jest ConcreteLogger
w tym przykładzie. Możliwym obejściem może być fabryka, która zwraca wymaganą implementację ILoggera.
public class RecordProcessor
{
public RecordProcessor()
{
Logger = new ConcreteLogger();
}
public ILogger Logger { get; set; }
public void DoSomeProcessing()
{
// ...
_logger.Log("Complete");
}
}
W większości przypadków wstrzykiwanie konstruktora jest lepsze niż wstrzykiwanie własności, ponieważ zapewnia lepsze gwarancje stanu obiektu natychmiast po jego zbudowaniu.
Metoda iniekcji
Metoda wstrzykiwania to drobnoziarnisty sposób wstrzykiwania zależności do przetwarzania. Rozważ metodę, która wykonuje pewne przetwarzanie w oparciu o bieżącą datę. Bieżącą datę trudno zmienić z testu, więc o wiele łatwiej jest przekazać datę do metody, którą chcesz przetestować.
public void ProcessRecords(DateTime currentDate)
{
foreach(var record in _records)
{
if (currentDate.Date > record.ProcessDate)
{
// Do some processing
}
}
}
Kontenery / szkielety DI
Wyodrębnianie zależności z kodu w celu ich wstrzyknięcia ułatwia testowanie kodu, ale przesuwa problem w górę hierarchii i może również powodować powstawanie obiektów trudnych do zbudowania. Różne ramy wstrzykiwania zależności / Inwersja kontenerów kontrolnych zostały napisane, aby pomóc rozwiązać ten problem. Umożliwiają rejestrację mapowań typów. Te rejestracje są następnie używane do rozwiązywania zależności, gdy kontener zostanie poproszony o zbudowanie obiektu.
Rozważ te klasy:
public interface ILogger {
void Log(string message);
}
public class ConcreteLogger : ILogger
{
public ConcreteLogger()
{
// ...
}
public void Log(string message)
{
// ...
}
}
public class SimpleClass
{
public SimpleClass()
{
// ...
}
}
public class SomeProcessor
{
public SomeProcessor(ILogger logger, SimpleClass simpleClass)
{
// ...
}
}
Aby zbudować SomeProcessor
, SomeProcessor
zarówno instancja ILogger
i SimpleClass
. Kontener taki jak Unity pomaga zautomatyzować ten proces.
Najpierw należy zbudować kontener, a następnie zarejestrować w nim mapowania. Zwykle odbywa się to tylko raz w aplikacji. Obszar systemu, w którym ma to miejsce, jest powszechnie znany jako Korzeń kompozycji
// Register the container
var container = new UnityContainer();
// Register a type mapping. This allows a `SimpleClass` instance
// to be constructed whenever it is required.
container.RegisterType<SimpleClass, SimpleClass>();
// Register an instance. This will use this instance of `ConcreteLogger`
// Whenever an `ILogger` is required.
container.RegisterInstance<ILogger>(new ConcreteLogger());
Po skonfigurowaniu kontenera można go używać do tworzenia obiektów, automatycznie rozwiązując zależności zgodnie z wymaganiami:
var processor = container.Resolve<SomeProcessor>();