In this talk we are going to show how TDD can help us in our daily work. Through a simple set of steps, we will see how it's possible to write a library without working before on any design. In this way, we will try to explain how are the tests who "suggests" us what to do in each state of development, how they make us focus to take baby steps in order to avoid drawbacks like paralysis by analysis.
Maybe you have done some programming katas (or maybe not) but you do not achieve to see what really TDD brings you. Or it is difficult to you see how to apply it in your daily basis. Whatever your level is, the goal of this talk is trying to show how it is possible to get a software emergent design by only applying some basic TDD rules with tiny unit tests.
--
En este evento vamos a mostrar cómo TDD puede ayudarnos en nuestro trabajo diario. Mediante una sencilla serie de pasos, veremos cómo se puede desarrollar una librería sin tener siquiera un prediseño pensado. De igual forma, intentaremos explicar cómo los tests nos van "sugiriendo" qué hacer en cada estado del desarrollo, cómo nos hacen enfocarnos para tomar pequeños pasos y evitar así, por ejemplo, la parálisis por análisis.
Tal vez hayas hecho algunas katas (o no) y no llegas a ver qué te aporta TDD, o no ves cómo aplicarlo a tu trabajo real, o te parece un poco pérdida de tiempo. Sea cual sea tu nivel, el objetivo es intentar demostrarte cómo practicando una técnica de TDD básica a base sólo de tests unitarios es posible conseguir un diseño emergente.
5. @enriquebarbeito
What is this talk about?
Software Craftsmanship Alicante
What is this talk about?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step
About the problem we need to solve
About Test-Driven Development
mytripcar/rentway step by step
What it is, how it works. Why?
TDD insights & rules I followed
How has it been designed?
6. @enriquebarbeito
What it is, how it works. Why?
Software Craftsmanship Alicante
What is this talk about?
What it is, how it works. Why? (1/4)
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step
As an API consumer,
I want to fetch Company
car rental rates so that
I can offer them along
with my other rates.
❏ We sign an agreement to work with a Company
❏ The Company provides a SOAP webservice system
❏ Our API needs to handle, send, receive information
from↔to their system, parse and improve it, and
return it to the caller/consumer.
❏ Big requirement that may be splitted.
7. @enriquebarbeito
What it is, how it works. Why?
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why? (2/4)
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step
Divide and conquer
❏ XML exchange messages
❏ HTTP connections handling
❏ Object-oriented (de)serialization
❏ SOAP services as an independent services
❏ Webservice client dispatcher
❏ Rental manager, analyzer, orchestrator
❏ API controllers and responses
Divide-conquer.png
8. @enriquebarbeito
What it is, how it works. Why?
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why? (3/4)
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step
tim-eric-mind-blown-gif
Fer Elisabet
Alberto
9. @enriquebarbeito
What it is, how it works. Why?
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why? (4/4)
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step
Thinking bottom-up to make a component
❏ XML exchange messages
❏ HTTP connections handling
❏ Object-oriented (de)serialization
❏ SOAP services as an independent services
❏ Webservice client dispatcher
❏ Rental manager, analyzer, orchestrator
❏ API controllers and responses
mytripcar/rentway
10. @enriquebarbeito
Test-Driven Development in a nutshell
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell (1/3)
Writing mytripcar/rentway step by step
Key concepts
❏ XP practice stated in 2003.
❏ TDD = TFD + Refactoring
❏ TDD != testing, so it is most about software design
❏ Does not replace architecture or design
❏ It is also a development process with a repetitive cycle.
Repeat with me: red-green-refactor! All-Code-Is-Guilty.jpg
11. @enriquebarbeito
Test-Driven Development in a nutshell
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell (2/3)
Writing mytripcar/rentway step by step
Key rules when doing TDD
❏ Ensure isolated specs
❏ Arrange. Act. Assert
❏ One verification per test/spec
❏ Write only one spec at a time
❏ Do not lose the green during a refactor step
❏ No debugging (neither output logging) TDD cycle
12. @enriquebarbeito
Test-Driven Development in a nutshell
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell (3/3)
Writing mytripcar/rentway step by step
Why TDD matters?
❏ Validates (proof) your design
❏ Provides quick feedback (does it work? is it simple
to use? is it well structured? is loosely coupled? ...)
❏ Enables you to: baby steps, focused in-flow, KISS
design, …
❏ Avoids: analysis by analysis, over-engineering, …
❏ Requires more discipline
❏ Gives confidence. Ease the change. Faster refactors
❏ … ERROR 1406: Data too long for column at slide 12^W
y7Hm9.jpg
13. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (1/27)
HEAD is now at 745c932... First commit
Stage 1
Focusing on how to start
14. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (2/27)
HEAD is now at 745c932... First commit
❏ New blank shiny project
❏ It starts with the minimal boilerplate
❏ No src/ tests/ folders
❏ 0% test code. 0% production code.
745c932... First commit
15. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (3/27)
8f9efd6 Add base Client class
❏ The million dollar questions: Where should I start? What should I test?
❏ Several starting points = Strategies of choice
❏ I chose the most basic: rentway is a (SOAP) client
❏ It was a good choice? Does it really matter?
test_client_object_should_be_created()
{
assertInstanceOf(Client::class, new Client);
}
16. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (4/27)
8f9efd6 Add base Client class
❏ Next million dollar question: What
should I test next?
❏ Listen to feedback each step gives.
❏ Change, rollback, delete code.
test_client_should_list_countries_from_ws()
{
client = new Client('testCompanyCode');
actual = client.getListCountries(true);
expected = this.resourceGetContents('rs_getListCountries.xml');
this.assertEquals(expected, actual);
}
test_client_should_get_multiple_prices_as_xml()
{
client = new Client('testCompanyCode', 'testCustomerCode',
'testUsername', 'testPassword');
actual = client.getMultiplePrices('testPickupDateTime',
'testPickupRentalStation', 'testDropoffDatetime',
'testDropoffRentalStation');
expected = this.resourceGetContents('rs_getListCountries.xml');
this.assertEquals(expected, actual);
}
17. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (5/27)
f7ec0c6 Add serializer dependency
❏ Client redesign: do not pass
credentials. Inject serializer indeed.
❏ How about credentials? Are ignored
for now.
❏ I delete code not related with tests.
test_client_object_should_be_created()
{
this.assertInstanceOf(Client::class, new Client(
new Serializer()
));
}
deleted:
- test_client_should_list_countries_from_ws()
- test_client_should_get_multiple_prices_as_xml()
18. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (7/27)
15cd51e Add message definitions for generic envelopes
Stage 2
Ignore Client and start thinking bottom-up
Focusing on Message (de)serialization
19. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (8/27)
15cd51e Add message definitions for generic envelopes
test_input_message_with_empty_body_serialize() {
actual = this.serializer.serialize(new Envelope(new Body), 'xml');
expected = this.resourceGetContents('rq_EmptyBody.xml');
this.assertEquals(expected, actual);
}
test_input_message_with_empty_body_deserialize() {
data = this.resourceGetContents('rq_EmptyBody.xml');
actual = this.serializer.deserialize(data, Envelope::class, 'xml');
expected = new Envelope(new Body);
this.assertEquals(expected, actual);
}
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body/>
</soap:Envelope>
tests/Resources/EmptyBody.xml
20. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (9/27)
6a3c3d5 Add GetCountries message
test_input_message_getCountries_serialize() {
getCountries = (new Envelope())
->setBody((new Body())
->setGetCountries(new GetCountries('testCompanyCode', true))
);
actual = this.serializer.serialize(getCountries, 'xml');
expected = this.resourceGetContents('rq_getCountries.xml');
this.assertEquals(expected, actual);
}
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<getCountries xmlns="http://⋯/⋯/getCountries">
<companyCode>testCompanyCode</companyCode>
<allCountries>true</allCountries>
</getCountries>
</soap:Body>
</soap:Envelope>
tests/Resources/rs_getCountries.xml
21. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (10/27)
cfab7da Refactor adding a Valuable trait
Key rules when doing TDD (bis)
Ensure isolated specs
Arrange. Act. Assert
One verification per test/spec
Write only one spec at a time
☺ Do not lose the green during a refactor step
No debugging (neither output logging)
22. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (11/27)
7f9c723 Improve message class composition
90d3c98 Finish GetCountries request (de)serialization
use MessagegetListCountries as Countries;
test_input_message_getCountries_serialize()
{
request = new CountriesRequest(
(new CountriesBody()).setGetCountries(
new CountriesGetCountries('testCompanyCode', true)
));
actual = this.serializer.serialize(request, 'xml');
expected = this.resourceGetContents('rq_getCountries.xml');
this.assertEquals(expected, actual);
}
23. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (12/27)
f6c602f Add factory method to getListCountries request
use MessagegetListCountries as Countries;
test_input_message_getCountries_serialize()
{
request = new CountriesRequest(
(new CountriesBody()).setGetCountries(
new CountriesGetCountries('testCompanyCode', true)
));
actual = this.serializer.serialize(request, 'xml');
expected = this.resourceGetContents('rq_getCountries.xml');
this.assertEquals(expected, actual);
}
use MessagegetListCountries as Countries;
test_input_message_getCountries_serialize()
{
request = CountriesRequest::create('testCompanyCode', true);
actual = this.serializer.serialize(request, 'xml');
expected = this.resourceGetContents('rq_getCountries.xml');
this.assertEquals(expected, actual);
}
24. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (13/27)
0cc3e9f Finish MultiplePrices request (de)serialization
package MessageCommon
package MessagegetMultiplePrices package MessagegetListCountries
26. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (15/27)
29ef5bd Finish getListCountries response (de)serialization
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<getCountriesResponse xmlns="http://⋯/⋯/getCountries">
<getCountriesResult>
<countries>
<diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"
xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
<Countries>
<Table diffgr:id="testTableId" msdata:rowOrder="0">
<countryID>1</countryID>
<country>testCountry</country>
<ISOCode>testIso</ISOCode>
</Table>
</Countries>
</diffgr:diffgram>
</countries>
</getCountriesResult>
</getCountriesResponse>
</soap:Body>
</soap:Envelope>
use MessagegetListCountries as Countries;
test_response_message_getListCountries_serialize() {
# Option 1: Repetitive request/response test design
response = CountriesResponse::create(
new CountriesCountryItem(1,'testCountry','testIso','testTableId', 0)
#, new CountriesCountryItem(...), new ...
);
actual = this.serializer.serialize(request, 'xml');
expected = this.resourceGetContents('rs_getCountries.xml');
this.assertEquals(expected, actual);
# Option 2: Using new test design
this.runTestSerializeWith(
'rs_getCountries.xml',
CountriesResponse::class,
new CountriesCountryItem(1,'testCountry','testIso','testTableId',0)
);
}
27. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (16/27)
0bdd3c4 Requests and responses refactor to interfaces
Remember the most important thing when refactor ...
☺ Do not lose the green during a refactor step
28. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (17/27)
1b2bce3 Inject http-client dependency in Client
Stage 3
Focusing on Client
29. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (18/27)
1b2bce3 Inject http-client dependency in Client
test_client_should_fetch_getListCountries_xml() {
data = this.resourceGetContents('rs_getCountries.xml');
httpClient = new HttpMockClient([data]);
client = new Client(httpClient, new Serializer);
expected = this.serializer->deserialize(data, CountriesResponse::class, 'xml');
actual = client.getListCountries(CountriesRequest::create(COMPANY_CODE, true));
this.assertEquals(expected, actual);
}
Design software
❏ Easily mockable
❏ Easily injectable
❏ Easily testable
30. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (19/27)
a31520a Add Client parameters and Client::getListCountries
use Configuration;
use Parameters;
test_parameters_configuration_should_be_created()
{
expectedHttp = new ConfigurationHttp(BASE_URL, BASIC_AUTH, TIMEOUT);
expectedCredentials = new ConfigurationCredentials(
COMPANY_CODE, CUSTOMER_CODE, USERNAME, PASSWORD);
actual = Parameters::create(
BASE_URL,
BASIC_AUTH,
TIMEOUT,
COMPANY_CODE,
CUSTOMER_CODE,
USERNAME,
PASSWORD
);
this.assertEquals(expectedHttp, actual.getHttp());
this.assertEquals(expectedCredentials, actual.getCredentials());
}
31. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (20/27)
a31520a Add Client parameters and Client::getListCountries
setEachTest() {
this.parameters = Parameters::create(
BASE_URL, BASIC_AUTH, TIMEOUT, COMPANY_CODE,
CUSTOMER_CODE, USERNAME, PASSWORD
);
}
test_client_should_fetch_getListCountries_xml() {
data = this.resourceGetContents('rs_getCountries.xml');
httpHandler = new HttpClient();
serializer = new Serializer();
client = new Client(this.parameters, httpClient, serializer);
expected = this.serializer.deserialize(data, CountriesResponse::class, 'xml');
actual = client.getListCountries(CountriesRequest::create(COMPANY_CODE, true));
this.assertEquals(expected, actual);
}
32. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (21/27)
1b2bce3 Inject http-client dependency in Client
Stage 4
Focusing on isolated Client results
33. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (22/27)
ab251c1 Client methods now return ResultInterface objects
test_result_from_XML_content_should_return_toJson() {
result = this.resultOf(CountriesResponse::class, 'rs_getListCountries.xml');
actual = result.toJson();
expected = this.resourceGetContents('rs_getListCountries.json');
this.assertEquals(expected, actual);
}
/**
* Returns a Result object of some ResponseInterface class with some data.
* @param string $classname
* @param string $filename
* @return Result
*/
private function resultOf(string classname, string filename): Result {
contents = this.resourceGetContents(filename);
format = extension_of(filename);
return new Result(classname, contents, format);
}
34. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (23/27)
ab251c1 Client methods now return ResultInterface objects
test_client_should_fetch_getListCountries_xml() {
data = this.resourceGetContents('rs_getCountries.xml');
httpHandler = new HttpClient();
serializer = new Serializer();
client = new Client(this.parameters, httpClient, serializer);
expected = this.serializer.deserialize(data, CountriesResponse::class, 'xml');
actual = client.getListCountries(CountriesRequest::create(COMPANY_CODE, true));
this.assertEquals(expected, actual->toObject());
}
35. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (24/27)
10c0925 Client refactored to isolated services
Stage 5
Focusing on independent Client services
36. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (25/27)
10c0925 Client refactored to isolated services
test_client_should_access_to_getListCountries_service() {
actual = this->client.getListCountries();
this.assertInstanceOf(ServiceInterface::class, actual);
this.assertInstanceOf(GetListCountries::class, actual);
}
test_client_should_access_to_getMultiplePrices_service() {
actual = this.client.getMultiplePrices();
this.assertInstanceOf(ServiceInterface::class, actual);
this.assertInstanceOf(GetMultiplePrices::class, actual);
}
// ClientTest rewrite. Previous tests moved to ServiceTest
test_should_fetch_error_getMultiplePrices_result() {
data = this.resourceGetContents('rs_getMultiplePrices_ko.xml');
client = this.mockClientWith(data);
expected = data;
actual = client.getMultiplePrices().request(MultiplePricesRequest::create(
new CommonCheckpoint('testDate1', 'testStation1'),
new CommonCheckpoint('testDate2', 'testStation1'),
COMPANY_CODE
));
this.assertEquals(expected, actual.toXml());
}
/**
* Returns a mocked instance of Client.
* @param string $data
* @return Client
*/
mockClientWith(string data) {
$httpClient = new HttpMockClient([data]);
return new Client(this.getParameters(), httpClient, this.getSerializer());
}
37. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (26/27)
cb53cdd Refactor all classes that implements ServiceInterface. Easier to add new ones
Remember the most important thing when refactor ...
☺ Do not lose the green during a refactor step
38. @enriquebarbeito
Writing mytripcar/rentway step by step
Software Craftsmanship Alicante
What, how, why?
What it is, how it works. Why?
Test-Driven Development in a nutshell
Writing mytripcar/rentway step by step (27/27)
10c0925 Now Client only works as a service dispatcher
shia-magic.gif