how tdd helps me design - a case study
TRANSCRIPT
Enrique Barbeito García
@enriquebarbeito
How TDD helps me designA case study
@enriquebarbeito
What is this talk about?What is this talk about?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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?
@enriquebarbeito
What it is, how it works. Why?What is this talk about?What it is, how it works. Why? (1/3)Test-Driven Development in a nutshellWriting 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.
@enriquebarbeito
What it is, how it works. Why?What, how, why?What it is, how it works. Why? (2/3)Test-Driven Development in a nutshellWriting 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
@enriquebarbeito
What it is, how it works. Why?What, how, why?What it is, how it works. Why? (3/3)Test-Driven Development in a nutshellWriting 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
@enriquebarbeito
Test-Driven Development in a nutshellWhat, 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
@enriquebarbeito
Test-Driven Development in a nutshellWhat, 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
@enriquebarbeito
Test-Driven Development in a nutshellWhat, 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^Wy7Hm9.jpg
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting mytripcar/rentway step by step (1/27)
HEAD is now at 745c932... First commit
Stage 1
Focusing on how to start
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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);}
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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);
}
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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()
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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)
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting mytripcar/rentway step by step (11/27)
7f9c723 Improve message class composition
90d3c98 Finish GetCountries request (de)serialization
use Message\getListCountries as Countries;
test_input_message_getCountries_serialize()
{
request = new Countries\Request(
(new Countries\Body()).setGetCountries(
new Countries\GetCountries('testCompanyCode', true)
));
actual = this.serializer.serialize(request, 'xml');
expected = this.resourceGetContents('rq_getCountries.xml');
this.assertEquals(expected, actual);
}
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting mytripcar/rentway step by step (12/27)
f6c602f Add factory method to getListCountries request
use Message\getListCountries as Countries;
test_input_message_getCountries_serialize()
{
request = new Countries\Request(
(new Countries\Body()).setGetCountries(
new Countries\GetCountries('testCompanyCode', true)
));
actual = this.serializer.serialize(request, 'xml');
expected = this.resourceGetContents('rq_getCountries.xml');
this.assertEquals(expected, actual);
}
use Message\getListCountries as Countries;
test_input_message_getCountries_serialize()
{
request = Countries\Request::create('testCompanyCode', true);
actual = this.serializer.serialize(request, 'xml');
expected = this.resourceGetContents('rq_getCountries.xml');
this.assertEquals(expected, actual);
}
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting mytripcar/rentway step by step (13/27)
0cc3e9f Finish MultiplePrices request (de)serialization
package Message\Common
package Message\getMultiplePrices package Message\getListCountries
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting mytripcar/rentway step by step (14/27)
e6566b0 Refactor around Message test suite test_input_message_getCountries_serialize() {
- request = Countries\Request::create('testCompanyCode', true);
-
- actual = $this.serializer.serialize(request, 'xml');
- expected = $this.resourceGetContents('rq_getCountries.xml');
-
- this.assertEquals(expected, actual);
+ this.runTestSerializeWith(
+ 'rq_getCountries.xml',
+ Countries\Request::class,
+ 'testCompanyCode',
+ true);
}
test_input_message_getCountries_deserialize() {
- data = $this->resourceGetContents('rq_getCountries.xml');
-
- actual = $this.serializer.deserialize(data, Countries\Request::class, 'xml');
- expected = Countries\Request::create('testCompanyCode', true);
-
- this.assertEquals(expected, actual);
+ this.runTestDeserializeWith(
+ 'rq_getCountries.xml',
+ Countries\Request::class,
+ 'testCompanyCode',
+ true);
}
+ /**
+ * @param string filename
+ * @param string classname
+ * @param array ...params
+ */
+ runTestDeserializeWith(string filename, string classname, ...params)
+ {
+ actual = this.deserializeFrom(filename, classname);
+ expected = classname::create(...params);
+
+ this.assertEquals(expected, actual);
+ }
+
+ /**
+ * @param string filename
+ * @param string classname
+ * @param array ...params
+ */
+ runTestSerializeWith(string filename, string classname, ...params)
+ {
+ actual = $this.serializeFrom(classname, ...params);
+ expected = $this.resourceGetContents(filename);
+
+ $this->assertEquals(expected, actual);
+ }
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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 Message\getListCountries as Countries;
test_response_message_getListCountries_serialize() {
# Option 1: Repetitive request/response test design
response = Countries\Response::create(
new Countries\CountryItem(1,'testCountry','testIso','testTableId', 0)
#, new Countries\CountryItem(...), 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',
Countries\Response::class,
new Countries\CountryItem(1,'testCountry','testIso','testTableId',0)
);
}
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting mytripcar/rentway step by step (17/27)
1b2bce3 Inject http-client dependency in Client
Stage 3
Focusing on Client
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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, Countries\Response::class, 'xml');
actual = client.getListCountries(Countries\Request::create(COMPANY_CODE, true));
this.assertEquals(expected, actual);
}
Design software
❏ Easily mockable
❏ Easily injectable
❏ Easily testable
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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 Configuration\Http(BASE_URL, BASIC_AUTH, TIMEOUT);
expectedCredentials = new Configuration\Credentials(
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());
}
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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, Countries\Response::class, 'xml');
actual = client.getListCountries(Countries\Request::create(COMPANY_CODE, true));
this.assertEquals(expected, actual);
}
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting mytripcar/rentway step by step (21/27)
1b2bce3 Inject http-client dependency in Client
Stage 4
Focusing on isolated Client results
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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(Countries\Response::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);
}
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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, Countries\Response::class, 'xml');
actual = client.getListCountries(Countries\Request::create(COMPANY_CODE, true));
this.assertEquals(expected, actual->toObject());
}
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting mytripcar/rentway step by step (24/27)
10c0925 Client refactored to isolated services
Stage 5
Focusing on independent Client services
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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(MultiplePrices\Request::create(
new Common\Checkpoint('testDate1', 'testStation1'),
new Common\Checkpoint('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());
}
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting 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
@enriquebarbeito
Writing mytripcar/rentway step by stepWhat, how, why?What it is, how it works. Why?Test-Driven Development in a nutshellWriting mytripcar/rentway step by step (27/27)
10c0925 Now Client only works as a service dispatcher
shia-magic.gif
Enrique Barbeito García
@enriquebarbeito
FIN. Thanks!Questions?
Please
, follow
!