Introduction to TDD
Jan. 19th 2016 – viscaweb.com/meetings
Before starting
I'll be presenting some old knowledge sprinkled with personal opinions and experiences
I'll try to present the pros and cons of everything we talk about
This is my take on this subject, I may be wrong in several points. Please feel free to voice your opinion and challenge my words!
#TDD
What is a test
Software testing is an investigation conducted to provide stakeholders with information about the quality of the product or service under test."
#TDD
Why do we want them?
- Reduce defects
- Increase confidence
- Reduce stress
- Make change cheap
- Improve efficiency
- And more...
#TDD
less fun time debugging :(
Why isn't everybody using them?
#TDD
Ideas?
Why isn't everybody using them?
#TDD
1) They are slow to write
They increase the initial cost, but reduce the cost of maintenance and change
Time/project constraints prevent us, but bugs will slow us down even more if we don't have them
Code that was not designed to be tested is difficult to test, but... fuck
#TDD
Legacy code dilemma
In order to refactor, we need to be sure that we are not breaking anything. We need tests to do so.
But in order to be able to test the code, we have to refactor.
We could do end-to-end tests, but that would stop our development efforts for months.
#TDD
Code discussion
final class Greeter
{
public function greet(string $name): void {
echo "Hi {$name}!";
}
}
$greeter = new Greeter();
$greeter->greet('Ricard');
"Code that was not designed to be tested is difficult to test"
#TDD
final class Greeter
{
public function greet(string $name): void {
echo "Hi {$name}!";
}
}
/** @test */
public function can_greet_a_particular_person()
{
$greeter = new Greeter();
$greeter->greet('Ricard');
$this->assertEquals('Hi Ricard!', $output);
}
#TDD
interface Printer {
public function output(string $value): void;
}
final class ConsolePrinter implements Printer {
public function output(string $value): void {
echo $value;
}
}
final class Greeter
{
/** @var Printer **/
private $printer;
public function __construct(Printer $printer) {
$this->printer = $printer;
}
public function greet(string $name): void {
$this->printer->output("Hi {$name}!");
}
}
#TDD
// Test code (w/ PHPUnit mocks)
$printer = $this->getMock(Printer::class);
$printer->expect($this->once())
->method('output')
->with('Hi Ricard!');
$greeter = new Greeter($output);
$greeter->greet('Ricard');
// Test code (w/ Prophecy)
$printer = $this->prophesize(Printer::class);
$greeter = new Greeter($printer->reveal());
$greeter->greet('Ricard');
$printer->output('Hi Ricard!')->shouldHaveBeenCalled();
// Production code
$greeter = new Greeter(new ConsolePrinter());
$greeter->greet('Ricard');
Why isn't everybody using them?
#TDD
2) They are difficult to write
They require experience katas!
They require knowledge ViscaMeetings, books, articles, talks
Even with some knowledge and experience, some areas are difficult to test Reduce the confidence by increasing abstraction. Tests will run faster, will be faster to write, but we'll be testing in different levels of abstraction.
#TDD
Design discussion
"Even with some knowledge and experience, some areas are difficult to test"
Let's pretend that we work for an elevator company.
How would we test the different combinations of floors?
#TDD
Design discussion (continued)
Which tests would give you the most confidence?
Which would be "good enough" while being fast to run and write?
How would you fake some situations which you cannot easily recreate in real life (closed floor, underground, etc.)?
#TDD
The cycle of TDD
#TDD
The cycle of TDD
-
Think about what you are going to test
-
Write a test
-
Wonder if the test is correct (rewrite until it is)
-
Run the test: red
-
Write the simplest implementation possible
-
Run the test: green
- Refactor, clean the mess we just created
#TDD
Start small
/** @test */
public function generates_first_batch_of_numbers()
{
$this->assertEquals(
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31],
generatePrimes()
);
}
#TDD
/** @test */
public function generates_up_to_first_number()
{
$this->assertEquals([2], generatePrimes());
}
// Red
function generatePrimes() {
}
// Green
function generatePrimes(): int[] {
return [2];
}
// Refactor? Not yet
#TDD
/** @test */
public function generates_up_to_second_number()
{
$this->assertEquals([2, 3], generatePrimes());
}
// Red
function generatePrimes(): int[] {
return [2];
}
// Green
function generatePrimes(): int[] {
return [2, 3];
}
// Refactor?
#TDD
When & why to refactor?
Fake it
Obvious implementation
Triangulate
(or third strike and refactor?)
#TDD
Start small
Don't try to accomplish too much, you might have to backtrack
Start with small steps, only increase complexity if you feel confident enough
#TDD
Discussion
Personal experiences with testing
What type of testing do you know, or currently use in LIFE/LIBE?
Why do you think some kinds of test have been discarded in the past?
#TDD
Hoare logic
- Precondition
- Command
- Postcondition
#TDD
Testing strategies
- Manual
- Automated
- Test-first
- Test-driven
#TDD
Testing strategies
Manual
- No regression test.
- Test is only checked once, and then the knowledge is lost
#TDD
Testing strategies
Automated
- Test is written after the fact. We are proving implementation as it is working now, but we may be wrong
- Time constraints and laziness will frequently prevent us from creating tests after writing the implementation. We are focused on delivering features, and testing interferes with that
#TDD
Testing strategies
Test-first
- We design upfront and then write tests that express that knowledge
- We are violating KISS and YAGNI
- We are going back to waterfall
#TDD
Testing strategies
Test-driven
- We write the code we wish we had, then implement it. JUST what we need.
- Emergent design
-
Serve as regression tests, they are not as thorough
#TDD
Effects on design
- Small, loosely coupled objects
- KISS
- YAGNI
#TDD
Types of tests
-
Unit
- A single part of your own code cut from every dependencies
- Different definition of unit depending on school of thought
-
Integration
- Same as unit test but for the code that must interact with an external component/library
-
Functional end-to-end test
- The whole application tested via the inputs/outputs
- System / application / acceptance
#TDD
Definition of unit
- Mockist: complete isolation from collaborators
- Classic: use real implementations if they are cheap to build. Tests act as mini-integration tests
#TDD
Speed of a test suite
- Slow
- Slow feedback loop
- Slow to run, which leads to running them less often
- Running them less often leads to writing more code without testing, which leads to defects
- Fast
- Fast feedback loop
- Can be run hundreds of times a day
- Every modification is immediately validated against existing assumption
#TDD
Test doubles
In automated unit testing, it may be necessary to use objects or procedures that look and behave like their release-intended counterparts, but are actually simplified versions that reduce the complexity and facilitate testing. A test double is a generic (meta) term used for these objects or procedures."
#TDD
Types of test doubles
- Dummy: objects are passed around but never actually used. Usually they are just used to fill parameter lists.
- Fake: actually have working implementations, but usually take some shortcut which makes them not suitable for production (an InMemoryTestDatabase is a good example).
- Stub: provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
- Spy: are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
- Mock: are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting.
#TDD
Black-box testing
Is a method of software testing that examines the functionality of an application without peering into its internal structures or workings. This method of test can be applied to virtually every level of software testing: unit, integration, system and acceptance. It typically comprises most if not all higher level testing, but can also dominate unit testing as well."
#TDD
White-box testing
(Also known as clear box testing, glass box testing, transparent box testing, and structural testing.)
Is a method of testing software that tests internal structures or workings of an application, as opposed to its functionality (i.e. black-box testing). In white-box testing an internal perspective of the system, as well as programming skills, are used to design test cases."
#TDD
Starting at the edges
-
Outside-in
- Mockist / London school / top-down
- Write with an acceptance test, start developing from the use case mocking the interactions with lower-level collaborators
-
Inside-out
- Classic / Chicago school / Detroit school / bottom-up
- Start small and work your way up
-
Middle-out
- Create a domain model, and connect to the necessary commands to make acceptance tests pass
#TDD
What else?
Lots more! This is too big of a topic, and will require years of experience, reading, pairing, etc., to discover which things work for you, your organization, your deadlines...
#TDD
Thanks!
Introduction to TDD - ViscaMeeting 2016-01-19
By ViscaWeb
Introduction to TDD - ViscaMeeting 2016-01-19
A brief introduction to different testing concepts and methodologies, focusing on Test-Driven Development (TDD) in particular.
- 3,300