Pokazywanie postów oznaczonych etykietą unit test. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą unit test. Pokaż wszystkie posty

sobota, 16 sierpnia 2014

[C#|Visual Studio] Rhino Mocks

Idealny Unit Test to taki, który jest:
  1. atomowy (testujemy tylko jeden fragment funkcjonalności)
  2. deterministyczny
  3. powtarzalny
  4. niezależny od kolejności wywoływania innych testów 
  5. szybki (czas wykonania rzędu milisekund)
  6. ł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)));
}

środa, 24 lipca 2013

[Visual Studio] Unit Testy

Pisząc Unit Testy należy pamiętać o zasadzie 3xA: Arrange Act Assert. Oznacza ona, że metody testujące powinny składać się z pewnego szablonu: na początku przypisujemy zmienne i oczekiwane wyjście, później wykonujemy test, a na końcu sprawdzamy, czy wynik jest poprawny. Wygodnym Frameworkiem do pisania Unit Testów w VS jest NUnitFramework. Instalujemy go za pomocą NuGeta.


Przykładowa metoda do przetestowania:

public class StringHelper
{
    public static int CountLetters(string word, char letter)
    {
        return word.Count(c => c == letter);
    }
}

...i odpowiadający mu UnitTest.

[TestFixture]
public class StringHelperTest
{
    [Test]
    public void CountLettersTest()
    {
        //Arrange
        var @string = "copacabana";
        var letter = 'a';
        int expected = 4;

        //Act
        int current = StringHelper.CountLetters(@string, letter);

        //Assert
        Assert.AreEqual(expected, current);
    }
}

Dla klas i metod obowiązuje pewna konwencja dodawania na końcu nazw słowa Test. Unit Testy można także bez problemu Debugować. Korzystając z dodatku Resharper można uruchamiać pojedyncze testy bezpośrednio w kodzie.


Przydatnymi atrybutami są SetUp i TearDown. Będą to metody wykonywane raz odpowiednio przed i po sesji testów.


[SetUp]
public void Init()
{
    Console.WriteLine("Initializing...");
}

[TearDown]
public void Clean()
{
    Console.WriteLine("Shutting down..."); 
}

Testy można także w razie potrzeby grupować w kategorie, również przez odpowiedni atrybut.

[Test]
[Category("another tests")]
public void CountLettersTest2()
{
    //Arrange
    var @string = "copACABana";
    var letter = 'a';
    int expected = 2;

    //Act
    int current = StringHelper.CountLetters(@string, letter);

    //Assert
    Assert.AreEqual(expected, current);
}

Dzięki temu z poziomu GUI możemy odpalać na przykład tylko testy z danej kategorii.


Ostatnia ciekawa funkcjonalność to timeouty. Możemy ustawić dla danej metody maksymalny czas wykonania poprzez atrybut MaxTime, np. [MaxTime(5000)].