Idealny Unit Test to taki, który jest:
- atomowy (testujemy tylko jeden fragment funkcjonalności)
- deterministyczny
- powtarzalny
- niezależny od kolejności wywoływania innych testów
- szybki (czas wykonania rzędu milisekund)
- łatwy do uruchomienia
Problemem zawsze będą zależności. Przeważnie metoda poddawana testowi woła metody innych klas, które mogą wykonywać wolne operacje, np. łączenie się z bazą danych. Co więcej testowanie metod w ten sposób narusza zasadę atomowości Unit Testów.
W tym przypadku z pomocą przychodzi zasada Dependency Inversion. Zamiast wstrzykiwać do testowanego serwisu właściwe zależności, możemy na etapie UT wstrzyknąć inną implementację danego interfejsu wykonującą tylko tyle kodu, ile potrzebujemy. Problem polega na tym, że wraz ze wzrostem komplikacji właściwych serwisów musimy także utrzymywać kod mockowanych serwisów.
Tu z pomocą przychodzi Rhino Mocks - framework budujący dynamiczne mocki (wykorzystując obiekty proxy) na potrzeby naszych testów.
Mockowanie rozpoczynamy od użycia klasy
MockRepository, która stworzy nam mock dla dowolnego interfejsu. Mock taki będzie zawierał puste metody (dla metod zwracających
void) lub metody zwracające wartość domyślną dla pozostałych metod z interfejsu.
Przykładowo mamy klasę
InvoiceService, która woła jedną ze swoich zależności -
InvoiceRepository.
public class Invoice
{
public string Id { get; set; }
public decimal Amount { get; set; }
public string UserId { get; set; }
}
public interface IInvoiceRepository
{
bool Store(Invoice entity);
}
public class InvoiceRepository : IInvoiceRepository
{
public bool Store(Invoice entity)
{
//Long running SQL operation
return true;
}
}
public class InvoiceService
{
private IInvoiceRepository _invoiceRepository;
public InvoiceService(IInvoiceRepository invoiceRepository)
{
_invoiceRepository = invoiceRepository;
}
public void SaveInvoice(Invoice item)
{
var success = _invoiceRepository.Store(item);
if(!success)
throw new Exception("Unable to save");
}
}
Chcemy zamockować interfejs
IInvoiceRepository w ten sposób, by zwracał
true, jeżeli podamy fakturę różną od
null.
[Test]
public static void InvoiceService_ShouldCallInvoiceRepository()
{
//Arrange
IInvoiceRepository repositoryMock = MockRepository.GenerateMock<IInvoiceRepository>();
repositoryMock.Stub(x => x.Store(Arg<Invoice>.Is.NotNull)).Return(true);
Debug.WriteLine(repositoryMock.GetType().FullName);
var service = new InvoiceService(repositoryMock);
//Act
service.SaveInvoice(new Invoice());
//Assert
repositoryMock.AssertWasCalled(s => s.Store(Arg<Invoice>.Is.Anything));
}
Na konsoli wypisze się:
Castle.Proxies.IInvoiceRepositoryProxyc9859a0ce17d461faef8b8deb39a407c, ponieważ RhinoMocks korzysta z mechanizmu
Castle.DynamicProxy.
Zamiast parametru
Anything moglibyśmy podać referencję do obiektu faktury przekazywanej do metody SaveInvoice. Często zdarza się też tak, że metoda przyjmuje kilka parametrów, na podstawie których budowany jest obiekt. Tu z pomocą przychodzi mechanizm constraintów.
Rozszerzamy nasz serwis o dodatkową metodę:
public class InvoiceService
{
//...
public void SaveInvoice(string id, string userId, decimal amount)
{
SaveInvoice(new Invoice()
{
Amount = amount,
Id = id,
UserId = userId
});
}
}
I za pomocą metody
Matches podajemy warunki, jakie muszą być spełnione.
[Test]
public static void InvoiceService_ShouldCallInvoiceRepository_AndSaveInvoiceWithSameProperties()
{
//Arrange
IInvoiceRepository repositoryMock = MockRepository.GenerateMock<IInvoiceRepository>();
repositoryMock.Stub(x => x.Store(Arg<Invoice>.Is.NotNull)).Return(true);
string id = "12";
string userId = "5";
decimal amount = 100;
var service = new InvoiceService(repositoryMock);
//Act
service.SaveInvoice(id, userId, amount);
//Assert
repositoryMock.AssertWasCalled(s => s.Store(Arg<Invoice>.Matches(d => d.Amount == amount
&& d.UserId == userId)));
}