slides

Unit Testing
You ain’t doing it, and you should
David Cantrell [email protected]
Chief Plumber, UK2 Ltd
Unit Testing
You ain’t doing it, and you should
David Cantrell [email protected]
Chief Plumber, UK2 Ltd
12:20 <@dc> ees me, ees mario!
12:21 <@ned> dc is promoted to chief plumber, in
charge of princess rescue and mushroom eating
Boss
This is your code
This is your code
This is your code
Let’s run a test
GET /foo/bar/baz
Let’s run a test
GET /foo/bar/baz
Let’s run a test
GET /foo/bar/baz
Let’s run a test
GET /foo/bar/baz
Let’s run a test
GET /foo/bar/baz
Let’s run a test
GET /foo/bar/baz
Let’s run a test
GET /foo/bar/baz
Let’s run a test
GET /foo/bar/baz
Let’s run a test
GET /foo/bar/baz
Let’s run a test
GET /foo/bar/baz
not ok 43572 GET /customer/94/invoice got right data
Where was that test failure?
Why do we test?
Why do we test?
•Testing new code
did I get this new feature right?
Why do we test?
•Testing new code
did I get this new feature right?
•Regression testing
did I break any existing code?
Why do we test?
•Testing new code
did I get this new feature right?
•Regression testing
did I break any existing code?
what did I break?
Why do we test?
•Testing new code
did I get this new feature right?
•Regression testing
did I break any existing code?
what did I break?
where?
The solution (sort of)
The solution (sort of)
The solution (sort of)
The solution (sort of)
and so on ad infinitum et tedium
That solution sucks
Great, you just tested most of
your code multiple times.
That solution sucks
Great, you just tested most of
your code multiple times.
Your tests took two hours.
That solution sucks
Great, you just tested most of
your code multiple times.
Your tests took two hours.
And again when you think you fixed them.
But that wasn’t unit testing
This is unit testing
This is unit testing
This is unit testing
This is unit testing
Repeat ad infinitum et tedium
This is unit testing
You just tested all of
your code once!
Your tests took twenty minutes!
That’s the end of the theory
Any questions?
Let’s look at some code
Let’s look at some code
(it doesn’t do much)
Let’s look at some code
End to end tests
package Dancer::Example::Calculator::Add;
use Dancer::Test;
use strict;
use warnings;
use WWW::Google::Calculator;
...
sub get {
my $class = shift;
my @numbers = @_;
die("illegal arguments") if(
grep { $_ !~ /^-?\d+(\.\d+)?$/ } @numbers
);
my $result = WWW::Google::Calculator->
new()->calc("$numbers[0] + $numbers[1]");
$result =~ s/.*= //;
return $result;
}
1;
response_status_is(
[ GET => '/add/2/lemon'],
500,
"GET /add/2/lemon - HTTP status is correct"
);
response_status_is(
[ GET => '/add/2/4'],
200,
"GET /add/2/4 - HTTP status is correct"
);
response_content_is(
[ GET => '/add/2/4'],
'6',
"GET /add/2/4 - content is correct"
);
End to end tests
package Dancer::Example::Calculator::Add;
use Dancer::Test;
use strict;
use warnings;
use WWW::Google::Calculator;
...
sub get {
my $class = shift;
my @numbers = @_;
die("illegal arguments") if(
grep { $_ !~ /^-?\d+(\.\d+)?$/ } @numbers
);
my $result = WWW::Google::Calculator->
new()->calc("$numbers[0] + $numbers[1]");
$result =~ s/.*= //;
return $result;
}
SKIP: {
skip "google isn't responding or its interface has changed", 3
unless(do {
my $r = eval { WWW::Google::Calculator->new()->calc('1+1') };
defined($r) && $r eq '1 + 1 = 2';
});
response_status_is(
[ GET => '/add/2/lemon'],
500,
"GET /add/2/lemon - HTTP status is correct"
);
response_status_is(
[ GET => '/add/2/4'],
200,
"GET /add/2/4 - HTTP status is correct"
);
response_content_is(
[ GET => '/add/2/4'],
'6',
"GET /add/2/4 - content is correct"
);
1;
}
End to end tests
package Dancer::Example::Calculator::Add;
use Dancer::Test;
use strict;
use warnings;
use WWW::Google::Calculator;
...
sub get {
my $class = shift;
my @numbers = @_;
die("illegal arguments") if(
grep { $_ !~ /^-?\d+(\.\d+)?$/ } @numbers
);
my $result = WWW::Google::Calculator->
new()->calc("$numbers[0] + $numbers[1]");
$result =~ s/.*= //;
return $result;
}
SKIP: {
skip "google isn't responding or its interface has changed", 3
unless(do {
my $r = eval { WWW::Google::Calculator->new()->calc('1+1') };
defined($r) && $r eq '1 + 1 = 2';
});
response_status_is(
[ GET => '/add/2/lemon'],
500,
"GET /add/2/lemon - HTTP status is correct"
);
response_status_is(
[ GET => '/add/2/4'],
200,
"GET /add/2/4 - HTTP status is correct"
);
response_content_is(
[ GET => '/add/2/4'],
'6',
"GET /add/2/4 - content is correct"
);
1;
}
$ time PERL5LIB=lib prove t/end-to-end.t
t/end-to-end.t .. ok
All tests successful.
Files=1, Tests=3, 8 wallclock secs ...
Get rid of the front-end layer
package Dancer::Example::Calculator::Add;
use strict;
use warnings;
use WWW::Google::Calculator;
sub get {
my $class = shift;
my @numbers = @_;
die("illegal arguments") if(
grep { $_ !~ /^-?\d+(\.\d+)?$/ } @numbers
);
my $result = WWW::Google::Calculator->new()->calc("$numbers[0] + $numbers[1]");
$result =~ s/.*= //;
return $result;
}
1;
throws_ok(
sub { Dancer::Example::Calculator::Add->get(1, 'lemons') },
qr/illegal arguments/, "catches non-numeric args"
);
...
is(Dancer::Example::Calculator::Add->get(2, 3), 5,
"Add->get() works for a simple case");
# Test that the error thrown above doesn't accidentally catch
# some legit data
is(Dancer::Example::Calculator::Add->get(-4, 2), -2,
"works for -ves too");
is(Dancer::Example::Calculator::Add->get(0.2, 1), 1.2,
"and decimals");
is(Dancer::Example::Calculator::Add->get(1, -2.2), -1.2,
"and -ve decimals");
Prepare to mock the call to Google
package Dancer::Example::Calculator::Add;
use strict;
use warnings;
use Class::Mockable
_google_calculator => 'Dancer::Example::Calculator::Utils::GoogleInterface';
use Dancer::Example::Calculator::Utils::GoogleInterface;
sub get {
my $class = shift;
my @numbers = @_;
die("illegal arguments") if(
grep { $_ !~ /^-?\d+(\.\d+)?$/ } @numbers
);
my $result = _google_calculator->calc("$numbers[0] + $numbers[1]”);
return $result;
}
1;
throws_ok(
sub { Dancer::Example::Calculator::Add->get(1, 'lemons') },
qr/illegal arguments/, "catches non-numeric args"
);
...
is(Dancer::Example::Calculator::Add->get(2, 3), 5,
"Add->get() works for a simple case");
# Test that the error thrown above doesn't accidentally catch
# some legit data
is(Dancer::Example::Calculator::Add->get(-4, 2), -2,
"works for -ves too");
is(Dancer::Example::Calculator::Add->get(0.2, 1), 1.2,
"and decimals");
is(Dancer::Example::Calculator::Add->get(1, -2.2), -1.2,
"and -ve decimals");
Prepare to mock the call to Google
package Dancer::Example::Calculator::Add;
use strict;
use warnings;
use Class::Mockable
_google_calculator => 'Dancer::Example::Calculator::Utils::GoogleInterface';
use Dancer::Example::Calculator::Utils::GoogleInterface;
sub get {
my $class = shift;
my @numbers = @_;
die("illegal arguments") if(
grep { $_ !~ /^-?\d+(\.\d+)?$/ } @numbers
);
my $result = _google_calculator->calc("$numbers[0] + $numbers[1]”);
return $result;
}
1;
package Dancer::Example::Calculator::Utils::GoogleInterface;
use strict;
use warnings;
use WWW::Google::Calculator;
sub calc {
my $class = shift;
my $sum
= shift;
(my $result = WWW::Google::Calculator->new()->calc($sum)) =~ s/.*= //;
return $result;
}
1;
Mock the call to Google
throws_ok(
sub { Dancer::Example::Calculator::Add->get(1, 'lemons') },
qr/illegal arguments/, "catches non-numeric args"
);
...
is(Dancer::Example::Calculator::Add->get(2, 3), 5,
"Add->get() works for a simple case");
# Test that the error thrown above doesn't accidentally catch
# some legit data.
is(Dancer::Example::Calculator::Add->get(-4, 2), -2,
"works for -ves too");
is(Dancer::Example::Calculator::Add->get(0.2, 1), 1.2,
"and decimals");
is(Dancer::Example::Calculator::Add->get(1, -2.2), -1.2,
"and -ve decimals");
Mock the call to Google
throws_ok(
sub { Dancer::Example::Calculator::Add->get(1, 'lemons') },
qr/illegal arguments/, "catches non-numeric args"
);
...
is(Dancer::Example::Calculator::Add->get(2, 3), 5,
"Add->get() works for a simple case");
# Test that the error thrown above doesn't accidentally catch
# some legit data. The bogus results prove we’re mocking.
is(Dancer::Example::Calculator::Add->get(-4, 2), 7,
"works for -ves too");
is(Dancer::Example::Calculator::Add->get(0.2, 1), 11,
"and decimals");
is(Dancer::Example::Calculator::Add->get(1, -2.2), -94,
"and -ve decimals");
Mock the call to Google
use Class::Mock::Generic::InterfaceTester;
Dancer::Example::Calculator::Add->_google_calculator(
Class::Mock::Generic::InterfaceTester->new([
{ method => 'calc', input => [ '2 + 3' ],
output
{ method => 'calc', input => [ '-4 + 2' ],
output
{ method => 'calc', input => [ '0.2 + 1' ], output
{ method => 'calc’, input => [ '1 + -2.2' ], output
])
);
=>
=>
=>
=>
5 },
7 },
11 },
-94 },
throws_ok(
sub { Dancer::Example::Calculator::Add->get(1, 'lemons') },
qr/illegal arguments/, "catches non-numeric args"
);
...
is(Dancer::Example::Calculator::Add->get(2, 3), 5,
"Add->get() works for a simple case");
# Test that the error thrown above doesn't accidentally catch
# some legit data. The bogus results prove we’re mocking.
is(Dancer::Example::Calculator::Add->get(-4, 2), 7,
"works for -ves too");
is(Dancer::Example::Calculator::Add->get(0.2, 1), 11,
"and decimals");
is(Dancer::Example::Calculator::Add->get(1, -2.2), -94,
"and -ve decimals");
Mock the call to Google
use Class::Mock::Generic::InterfaceTester;
Dancer::Example::Calculator::Add->_google_calculator(
Class::Mock::Generic::InterfaceTester->new([
{ method => 'calc', input => [ '2 + 3' ],
output
{ method => 'calc', input => [ '-4 + 2' ],
output
{ method => 'calc', input => [ '0.2 + 1' ], output
{ method => 'calc’, input => [ '1 + -2.2' ], output
])
);
=>
=>
=>
=>
5 },
7 },
11 },
-94 },
throws_ok(
sub { Dancer::Example::Calculator::Add->get(1, 'lemons') },
qr/illegal arguments/, "catches non-numeric args"
);
...
is(Dancer::Example::Calculator::Add->get(2, 3), 5,
"Add->get() works for a simple case");
# Test that the error thrown above doesn't accidentally catch
# some legit data. The bogus results prove we’re mocking.
is(Dancer::Example::Calculator::Add->get(-4, 2), 7,
"works for -ves too");
is(Dancer::Example::Calculator::Add->get(0.2, 1), 11,
"and decimals");
is(Dancer::Example::Calculator::Add->get(1, -2.2), -94,
"and -ve decimals");
$ time PERL5LIB=lib prove t/add.t
t/add.t .. ok
All tests successful.
Files=1, Tests=8, 0 wallclock secs ...
Any questions?
Any questions?
Isn’t mocking dangerous? Surely you need to test your whole
application?
Any questions?
Isn’t mocking dangerous? Surely you need to test your whole
application?
Yes. You still need some end-to-end tests.
Any questions?
Isn’t mocking dangerous? Surely you need to test your whole
application?
Yes. You still need some end-to-end tests.
Mocking everything seems like an awful lot of work.
Any questions?
Isn’t mocking dangerous? Surely you need to test your whole
application?
Yes. You still need some end-to-end tests.
Mocking everything seems like an awful lot of work.
That’s not a question.
Any questions?
Isn’t mocking dangerous? Surely you need to test your whole
application?
Yes. You still need some end-to-end tests.
Mocking everything seems like an awful lot of work.
Be pragmatic about what you mock, and think about
why you mock. At least make your dependencies and
interfaces mockable, even if you don’t actually do
any mocking straight away.
Any questions?
Isn’t mocking dangerous? Surely you need to test your whole
application?
Yes. You still need some end-to-end tests.
Mocking everything seems like an awful lot of work.
Be pragmatic about what you mock, and think about
why you mock. At least make your dependencies and
interfaces mockable, even if you don’t actually do
any mocking straight away.
Have you seen the new Doctor Who?
Any questions?
Isn’t mocking dangerous? Surely you need to test your whole
application?
Yes. You still need some end-to-end tests.
Mocking everything seems like an awful lot of work.
Be pragmatic about what you mock, and think about
why you mock. At least make your dependencies and
interfaces mockable, even if you don’t actually do
any mocking straight away.
Have you seen the new Doctor Who?
No. Doctor Who is rubbish.
Resources
Class::Mockable
The Art of Unit Testing, by Roy Osherove
pub: Manning
ISBN: 1933988274