Dzisiaj chciałbym przedstawić działanie narzędzia do mockowania Telerik JustMock. Dostępne są dwie wersje tego narzędzia: darmowa JustMock Free Edition oraz komercyjna, pełne porównanie można znaleźć tutaj: http://www.telerik.com/products/mocking/free.aspx. Główną motywacją do napisania tego postu było to, że czytając posta Macieja Aniserowicza (oczywiście polecam Macieja serię postów o testowaniu: http://www.maciejaniserowicz.com/post/2011/08/08/UT-0-Zapowiedz-minicyklu-o-testach.aspx) zauważyłem w komentarzach, że wiele osób nie do końca rozumie co mockować w testach.
Na początek co warto mockować?
Mockować warto i należy wszystkie elementy, które współpracują ze światem zewnętrznym, czyli web serwisy, dostęp do bazy danych itd. Tak jak sama nazwa mówi “testy jednostkowe”, czyli chcemy testować tylko daną jednostkę/klasę bez zewnętrznych zależności, dodatkowo mockowanie zależności zewnętrznych zapewnia nam powtarzalność testów, ponieważ dane pochodzące z zewnątrz mogą się zmieniać.
Dlaczego JustMock?
JustMock posiada wiele interesujących funkcji, których nie znajdziemy u konkurencji, takich jak mockowanie zapytań LINQ, mockowanie pojedynczych metod bez mockowania całej klasy. Najfajniejsze funkcje dostępne są tylko w wersji komercyjnej, ale wydanie tych 400 dolców jest dobrą inwestycją, ponieważ w zamian otrzymuje porządne narzędzie. W poniższym przykładach użyłem wersji darmowej, ponieważ funkcje w niej dostępne w tutaj całkowicie wystarczą.
Założenia i testowana klasa
Celem naszego przykładu jest stworzenie klasy, która służy do przeliczania kwoty pomiędzy dwoma wybranymi walutami, wykorzystując w tym celu tabelę referencyjną wartości walut w stosunku do Euro z Europejskiego Banku Centralnego (tabela: http://www.ecb.int/stats/exchange/eurofxref/html/index.en.html oraz tabela w wersji XML: http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml). Walutą bazową dla tabeli i naszych obliczeń jest Euro.
W celu obliczenia kwoty w docelowej walucie musimy użyć poniższego wzoru:
Kwota w walucie docelowej = (kwota w walucie źródłowej * wartość waluty docelowej w Euro) / wartość waluty źródłowej w Euro
Testować będziemy klasę CurrencyService, która jest odpowiedzialna za wymienianie kwoty między walutami i wygląda jak poniżej:
public class CurrencyService
{
private readonly IBankServiceProxy bankServiceProxy;
private Currency[] cachedCurrencies;
public CurrencyService(IBankServiceProxy bankServiceProxy)
{
this.bankServiceProxy = bankServiceProxy;
}
public decimal Exchange(string sourceCurrencySymbol, string destinationCurrencySymbol, decimal amount)
{
if (this.cachedCurrencies == null)
{
this.cachedCurrencies = this.bankServiceProxy.GetCurrencies();
}
Currency sourceCurrency = this.cachedCurrencies.SingleOrDefault(c => c.Symbol.Equals(sourceCurrencySymbol));
Currency destinationCurrency = this.cachedCurrencies.SingleOrDefault(c => c.Symbol.Equals(destinationCurrencySymbol));
sourceCurrency.ThrowArgumentOutOfRangeIfNull("sourceCurrency", "Currency doesn't exists: " + sourceCurrencySymbol);
destinationCurrency.ThrowArgumentOutOfRangeIfNull("destinationCurrency", "Currency doesn't exists: " + destinationCurrencySymbol);
return (amount * destinationCurrency.Rate) / sourceCurrency.Rate;
}
}
Użyłem w niej rozszerzenia ThrowArgumentOutOfRangeIfNull(…), które implementacja wygląda jak poniżej:
public static class ObjectExtensions
{
public static void ThrowArgumentOutOfRangeIfNull(this object obj, string parameterName, string message)
{
if (obj == null)
{
throw new ArgumentOutOfRangeException(parameterName, message);
}
}
}
Natomiast interfejs IBankServiceProxy wygląda tak:
public interface IBankServiceProxy
{
Currency[] GetCurrencies();
}
Oraz klasa Currency:
public class Currency
{
public Currency(string symbol, decimal rate)
{
this.Symbol = symbol;
this.Rate = rate;
}
public string Symbol { get; private set; }
public decimal Rate { get; private set; }
}
Pisanie testów
Celem naszych testów będzie sprawdzenie metody Exchange klasy CurrencyService. Testowana klasa wymaga obiektu implementującego interfejs IBankServiceProxy w celu pobierania informacji o kursie walut i tą właśnie klasę będziemy mockować.
Stworzenie mocka implementującego interfejs IBankServiceProxy w JustMock polega na wywołaniu Mock.Create<IBankServiceProxy>(), która stworzy nowy obiekt. Stworzony obiekt należy następnie skonfigurować tak, żeby po wywołaniu metody GetCurrencies() zwracał zdefiniowaną tablicę zawierającą kursy walut, zrobimy to wywołując Mock.Arrange(() => bankServiceProxyMock.GetCurrencies()).Returns(currencies), gdzie currencies jest naszą tablicą zawierającą kursy walut:
Currency[] currencies = new Currency[]
{
new Currency("USD", 1.500m),
new Currency("PLN", 4.000m),
new Currency("GBP", 0.800m)
};
IBankServiceProxy bankServiceProxyMock = Mock.Create<IBankServiceProxy>();
Mock.Arrange(() => bankServiceProxyMock.GetCurrencies()).Returns(currencies);
Następną rzeczą, którą musimy zrobić jest stworzenie obiektu klasy CurrencyService podając jako parametr naszego mocka i wywołując wywołując funkcję Exchange z odpowiednimi parametrami sprawdzamy czy wartość zwrócona przez Exchange jest poprawna:
CurrencyService currencyService = new CurrencyService(bankServiceProxyMock);
decimal amountAfterExchange = currencyService.Exchange(“USD”, “GBP”, 120m);
Assert.AreEqual(64m, amountAfterExchange);
W teście użyłem atrybutu TestCase z NUnit, który umożliwia podanie parametrów przekazywanych do metody testującej, dzięki temu możemy testować wiele wariantów za pomocą pojedynczego testu. Cały test wygląda tak:
[TestFixture]
public class CurrencyServiceFixture
{
private Currency[] currencies;
public CurrencyServiceFixture()
{
this.currencies = new Currency[]
{
new Currency("USD", 1.500m),
new Currency("PLN", 4.000m),
new Currency("GBP", 0.800m)
};
}
[TestCase("USD", "GBP", 120.0, 64.0)]
[TestCase("USD", "PLN", 60.0, 160.0)]
[TestCase("PLN", "USD", 200.0, 75.0)]
[TestCase("PLN", "GBP", 200.0, 40.0)]
[TestCase("GBP", "USD", 80.0, 150.0)]
[TestCase("GBP", "PLN", 30.0, 150.0)]
public void ShouldExchangeAmountBetweenCurrencies(string sourceCurrencySymbol, string destinationCurrencySymbol, decimal amount, decimal expectedAmount)
{
IBankServiceProxy bankServiceProxyMock = Mock.Create<IBankServiceProxy>();
Mock.Arrange(() => bankServiceProxyMock.GetCurrencies()).Returns(currencies);
CurrencyService currencyService = new CurrencyService(bankServiceProxyMock);
decimal amountAfterExchange = currencyService.Exchange(sourceCurrencySymbol, destinationCurrencySymbol, amount);
Assert.AreEqual(expectedAmount, amountAfterExchange);
}
[TestCase("USD", "ABC")]
[TestCase("ABC", "USD")]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ShouldThrowArgumentOfRangeWhenCurrencyDoesntExists(string sourceCurrencySymbol, string destinationCurrencySymbol)
{
IBankServiceProxy bankServiceProxyMock = Mock.Create<IBankServiceProxy>();
Mock.Arrange(() => bankServiceProxyMock.GetCurrencies()).Returns(currencies);
CurrencyService currencyService = new CurrencyService(bankServiceProxyMock);
currencyService.Exchange(sourceCurrencySymbol, destinationCurrencySymbol, 100);
}
}
Jak widać testujemy zarówno ścieżkę pozytywną wykonania użycia funkcji Exchange, gdy symbole walut są poprawne oraz gdy są błędne.
Podsumowanie
W powyższym przykładzie można zauważyć, że mocki pomagają nie tylko testować klasy, których zależności są jeszcze nie zaimplementowane, ale pomniejszają zależności testowanej klasy od klas, od których zależy oraz czynników zewnętrznych (w tym przypadku zmiany kursu walut).
Tradycyjnie przykładowy projekt został dołączony poniżej:

a czy znasz jakiś sposób aby mockować serwisy na platformie Windows Phone 7? Jakiś framework lub ciekawy sposób?
Hmm, możesz spróbować dodać projekt „Silverlight Class Library” do solucji i w nim dodać referencje do testowanego projektu WP7. Wtedy będziesz mógł użyć JustMocka dla Silverlighta (referencja do Telerik.JustMock.Silverlight.dll lub z poziomu NuGet: Install-Package JustMock).
Daj znać czy się udało