O Consumer-Driven-Contracts pisałem już tutaj. Sam koncept jest dosyć prosty, ale bez odpowiednich narzędzi może być mozolny w wdrożeniu.
Pack-net, jak nie trudno się domyślić jest implementacją biblioteki pact w C#. Kod źródłowy możną znaleźć na GitHub.
Wiemy, aby zaimplementować Consumer-Driven-Contracts (CDC) musimy napisać testy zarówno po stronie konsumenta jak i dostawcy. Zwykle, konsumenci nie testują prawdziwej usługi, a jedynie operują na mock’ach. Następnie dostawca, wykona wszystkie testy konsumenckie na prawdziwej usłudzę. Sztuczka polega na tym, aby w jakiś sposób przekazać do dostawcy testy zdefiniowane przez konsumenta. Nie chcemy generować biblioteki DLL i jej przekazywać. Pact wygeneruje za nas plik JSON, ze wszystkimi testami. Podsumowując za pomocą Pact:
1. Konsumenci definiują klasyczne testy jednostkowe. Wykonywane są one w izolacji na mock’u usługi.
2. Testy jednostkowe są “nagrywane” i zapisywane w pliku JSON.
3. Plik (lub pliki w przypadku wielu konsumentów) JSON są odczytywane przez producenta. Nagrane testy są odtwarzane, ale będą już wykonywane na prawdziwej usłudze.
Jeśli tworzymy usługę, wtedy krok trzeci gwarantuje nam, że spełniamy wszystkie wymogi konsumentów. Jeśli konsument A potrzebuje np. pole FirstName w odpowiedzi, wtedy zdefiniuje to za pomocą testu jednostkowego, a dostawca następnie zweryfikuje to.
Pact dostarcza łatwy interfejs i nie musimy się martwić np. odpaleniem serwer’a web w testach dostawcy, które zawsze wykonywane są na prawdziwej usłudze. Podobnie, dzięki Pact łatwo tworzyć mock’a usługi. Po prostu wystarczy zdefiniować oczekiwane nagłówki czy zawartość ciała.
Myślę, że najłatwiej będzie to zrozumieć na przykładzie. Załóżmy, że tworzymy usługę zwracającą listę osób. Nasza solucja zatem będzie składać się z:
1. Provider.Web – usługa
2. Provider.Web.Tests – testy dostawcy (wykonywane na prawdziwej usłudze)
3. Consumer – prosty klient łączący się z usługą
4. Consumer.Tests – testy konsumenckie (wykonywane na mock’u)
W dzisiejszym wpisie zajmiemy się punktami 1,3 oraz 4, a następnym razem zajmiemy się testami dostawcy.
Zacznijmy zatem od dostawcy (punkt 1):
public class PersonsController : ApiController
{
public PersonInfo[] GetAll()
{
var person1 = new PersonInfo {FirstName = "Piotr", LastName = "Zielinski"};
var person2 = new PersonInfo { FirstName = "first name", LastName = "last name" };
return new PersonInfo[] {person1, person2};
}
}
public class PersonInfo
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
Zwykłe WebApi, którego odpowiedź wygląda następująco:
<ArrayOfPersonInfo xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Provider.Web.Controllers">
<PersonInfo>
<FirstName>Piotr</FirstName>
<LastName>Zielinski</LastName>
</PersonInfo>
<PersonInfo>
<FirstName>first name</FirstName>
<LastName>last name</LastName>
</PersonInfo>
</ArrayOfPersonInfo>
Następnie zdefiniujemy klienta (konsumenta – punkt 3):
public class PersonsApiClient
{
public string BaseUri { get; set; }
public PersonsApiClient(string baseUri)
{
BaseUri = baseUri;
}
public PersonInfo[] GetAllPersons()
{
var client = new RestClient(BaseUri);
var persons = client.Get<List<PersonInfo>>(new RestRequest("api/persons")).Data;
return persons.ToArray();
}
}
Ostatni etap w dzisiejszym wpisie to zdefiniowanie testu konsumenta (punkt 4):
[TestFixture]
public class ConsumerTests
{
private IMockProviderService _mockProviderService;
private IPactBuilder _pactProvider;
[OneTimeSetUp]
public void OneTimeSetUp()
{
_pactProvider = new PactBuilder()
.ServiceConsumer("ConsumerName")
.HasPactWith("ProviderName");
_mockProviderService = _pactProvider.MockService(3423,false);
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
_pactProvider.Build();
}
[Test]
public void Should_Return_FirstName()
{
//Arrange
_mockProviderService.Given("there are some persons data")
.UponReceiving("a request to retrieve all persons")
.With(new ProviderServiceRequest
{
Method = HttpVerb.Get,
Path = "/api/persons"
})
.WillRespondWith(new ProviderServiceResponse
{
Status = 200,
Headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json; charset=utf-8" }
},
Body = new[]
{
new
{
FirstName = "Piotr",
}
}
});
var consumer = new PersonsApiClient("http://localhost:3423");
//Act
var persons = consumer.GetAllPersons();
//Assert
CollectionAssert.IsNotEmpty(persons);
CollectionAssert.AllItemsAreNotNull(persons.Select(x=>x.FirstName));
_mockProviderService.VerifyInteractions();
}
}
Za pomocą klasy PactBuilder budujemy mock usługi czyli IMockProviderService. Następnie definiujemy wejścia i wyjścia mocka. Widzimy, że jeśli zapytanie GET przyjdzie na /api/persons wtedy zwracamy jeden obiekt, zawierający imię. Za pomocą MockProvider możemy zasymulować dowolne zachowanie usługi. W naszym przypadku jest to po prostu GET i odpowiedź. W bardziej zaawansowanych przypadkach dochodzą do tego parametry query, nagłówki, metody POST, DELETE itp.
Mock zwróci zatem tylko jeden obiekt, zawierający imię Za pomocą następnych asercji, zweryfikujemy, że faktycznie imię musi zawierać jakąś treść. Jeśli pole FirstName na tym etapie jest puste, oznacza to, że implementacja klienta jest błędną – np. deserialziacja nie działa tak jak należy. Widzimy, że port 3423 został dobrany losowo, ponieważ nie ma to znaczenia dla mock’a.
VerifyInteractions sprawdzi również tzw. interakcje. W powyższym przypadku oznacza to, że dokładnie jedno zapytanie GET “api/persons” zostało wysłane do usługi. Jeśli nasz klient zawierałby błąd, np. dwukrotne wysłanie zapytania, wtedy test również zakończyłby się błędem.
Po wykonaniu testu, zostanie on nagrany i zapisany w pliku “consumername-providername.json”:
{
"provider": {
"name": "ProviderName"
},
"consumer": {
"name": "ConsumerName"
},
"interactions": [
{
"description": "a request to retrieve all persons",
"provider_state": "there are some persons data",
"request": {
"method": "get",
"path": "/api/persons"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"body": [
{
"FirstName": "Piotr"
}
]
}
}
],
"metadata": {
"pactSpecificationVersion": "1.1.0"
}
}
W kolejnym wpisie zajmiemy się implementacją testów dostawcy, które będą wykorzystywać właśnie wygenerowany plik JSON, w celu wykonania takiego samego testu na prawdziwej usłudze.