/
SampleArticle.html
193 lines (136 loc) · 7.81 KB
/
SampleArticle.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Separate data and behavior with table-driven testing</title>
<meta name="description" content="Table-driven testing reduces code and lowers the cost of testing. This article shows how to use table-driven testing with Perl.">
<meta name="author" content="brian d foy">
<meta name="keywords" content="unit,testing,data,table,perl,tdd">
</head>
<body>
<img src="onion_charcoal.png" alt="Perl Onion" class="cover">
<div class="title">Separate data and behavior with table-driven testing</div>
<div class="subtitle">Applying DRY to unit testing</div>
<div class="publish-date">2000-12-31T00:00:00Z</div>
<p class="author-bio"><a href="http://www.pair.com/~comdog/">brian d foy</a> (<a href="https://twitter.com/briandfoy_perl">briandfoy_perl</a>) is a <a href="https://en.wikipedia.org/wiki/Perl">Perl</a> trainer and writer. He's the author of Mastering Perl and co-author of Programming Perl, Learning Perl, Intermediate Perl and Effective Perl Programming. Github: <a href="https://github.com/briandfoy/projects">projects</a>.</p>
<img src="http://static/images/brian_d_foy.png" alt="http://static/images/brian_d_foy.png" class="author-image">
<p>How can I easily run the same tests on different data without duplicating a lot of code? If I follow my usual pattern, I start off with a couple of tests where I write some code then cut-and-paste that a couple of times. I add a few more tests before I realize I have a mess. If I had the foresight to know that I would make a mess (again), I would have started with a table of data and a little bit of code that went through it.<sup><a href="#0_1">1</a></sup></p>
<p>Consider a silly and small example of testing <code>sprintf</code>-like behaviour of M<String::Sprintf>. I can use this module to create my own format specifiers, such as one to commify a number. I stole this mostly from its documentation, although I threw in the <a href="http://www.effectiveperlprogramming.com/2015/04/use-v5-20-subroutine-signatures/">v5.20 signatures feature</a> and the <a href="http://www.effectiveperlprogramming.com/2010/09/use-the-r-substitution-flag-to-work-on-a-copy/">v5.14 non-destructive</a> <code>/r</code> flag on the substitution operator> because I love those features:</p>
<pre>use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);
use String::Sprintf;
my $f = String::Sprintf->formatter(
N => sub {
my($width, $value, $values, $letter) = @_;
return commify(sprintf "%${width}f", $value);
}
);
say "Numbers are: " .
$f->sprintf(
'%10.2N, %10.2N',
12345678.901, 87654.321
);
sub commify ( $n ) {
$n =~ s/(\.\d+)|(?<=\d)(?=(?:\d\d\d)+\b)/$1 || ','/rge;
}
Numbers are: 12,345,678.90, 87,654.32</pre>
<p>The mess I might make to test this starts with a single input and output with the M<Test::More> function <code>is</code>:</p>
<pre>use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);
use Test::More;
sub commify ( $n ) {
$n =~ s/(\.\d+)|(?<=\d)(?=(?:\d\d\d)+\b)/$1 || ','/rge;
}
my $class = 'String::Sprintf';
use_ok( $class );
my $f = String::Sprintf->formatter(
N => sub {
my($width, $value, $values, $letter) = @_;
return commify(sprintf "%${width}f", $value);
}
);
isa_ok( $f, $class );
can_ok( $f, 'sprintf' );
is( $f->sprintf( '%.2N', '1234.56' ), '1,234.56' );
done_testing();</pre>
<p>I decide to test another value, and I think the easiest thing to do is to duplicate that line with <code>is</code>:</p>
<pre>is( $f->sprintf( '%.2N', '1234.56' ), '1,234.56' );
is( $f->sprintf( '%.2N', '1234' ), '1,234.00' );</pre>
<p>The particular thing to test isn't the point of this article. It's all the stuff around it that I want to highlight. Or, more correctly, I want to de-emphasize all this stuff around it. I had to duplicate the test although most of the structure is the same.</p>
<p>I can convert those tests to a structure to hold the data and another structure for the behaviour:</p>
<pre>my @data = (
[ qw( 1234.56 1,234.56 ) ],
[ qw( 1234 1,234.00 ) ],
);
foreach my $row ( @data ) {
is( $f->sprintf( '%.2N', $row->[0] ), $row->[1] );
}</pre>
<p>I can add many more rows to <code>@data</code> but the meat of the code, that <code>foreach</code> loop, doesn't change.</p>
<p>I can improve this though. So far I only test that one template. I can add that to <code>@data</code> too, and use that to make a label for the test:</p>
<pre>my $ndot2_f = '%.2N';
my @data = (
[ $ndot2_f, qw( 1234.56 1,234.56 ) ],
[ $ndot2_f, qw( 1234 1,234.00 ) ],
);
foreach my $row ( @data ) {
is( $f->sprintf( $row->[0], $row->[1] ), $row->[2],
"$row->[1] with format $row->[0] returns $row->[2]"
);
}</pre>
<p>I can add another test with a different format. If I had kept going the way I started, this would look like a new test because the format changed. Now the format is just part of the input:</p>
<pre>my $ndot2_f = '%.2N';
my @data = (
[ $ndot2_f, qw( 1234.56 1,234.56 ) ],
[ $ndot2_f, qw( 1234 1,234.00 ) ],
[ '%.0N' , qw( 1234.49 1,234 ) ],
);
foreach my $row ( @data ) {
is( $f->sprintf( $row->[0], $row->[1] ), $row->[2],
"$row->[1] with format $row->[0] returns $row->[2]"
);
}</pre>
<p>As I go on things get more complicated. If a test fails, I want some extra information about which one failed. I'll change up how I go through the table. In this case, I'll use the <a href="http://www.effectiveperlprogramming.com/2010/05/perl-5-12-lets-you-use-each-on-an-array/">v5.12 feature</a> that allows <code>each</code> on an array so I get back the index and the value:</p>
<pre>while( my( $index, $row ) = each @data ) {
is( $f->sprintf( $row->[0], $row->[1] ), $row->[2],
"$index: $row->[1] with format $row->[0] returns $row->[2]"
);
}</pre>
<p>My code for the test behavior changed but I didn't have to mess with the input data at all. The particular code in this case doesn't matter. This table-driven testing separates the inputs and the tests; that's what you should pay attention to.</p>
<p>It can get even better. So far, I've put all the input data in the test file itself, but now that it's separate from the test code, I can grab the input from somewhere else, like a database table:</p>
<table>
<th>ColA</th><th>ColB</th><th>ColC</th>
<tr><td>%.2N</td><td>1234.56</td><td>1,234.56</td></tr>
<tr><td>%.2N</td><td>1234</td><td>1,234.00</td></tr>
<tr><td>%.0N</td><td>1234.49</td><td>1,234</td></tr>
</table>
<p>Or a pipe separated values file:</p>
<pre class="data">%.2N|1234.56|1,234.56
%.2N|1234|1,234.00
%.0N|1234.49|1,234</pre>
<p>I create <code>@data</code> in the test file by reading and parsing the external file <span class="data">tests.t</span>:</p>
<pre><code>open my $test_data_fh, '<', $test_file_name or die ...;
my @data;
while( <$test_data_fh> ) {
chomp;
push @data, split /\t/;
}</code></pre>
<p>I can execute the tests with <span class="terminal">perl tests.t</span> Now none of the data are in the test file. And, there's nothing special about a simple text file. I could do a little bit more work to take the data from an Excel file (perhaps that most useful wizard skill in the world of business) or even a database:</p>
<pre><code>use DBI;
my $dbh = DBI->connect( ... );
my $sth = $dbh->prepare( 'SELECT * FROM tests' );
$sth->execute();
while( my $row = $sth->fetchrow_arrayref ) {
state $index = 0;
is( $f->sprintf( $row->[0], $row->[1] ), $row->[2],
$index++ . ": $row->[1] with format $row->[0] returns $row->[2]"
);
}</code></pre>
<p>That's the idea. I separate the data and the tests to give myself some flexibility. How I access the data and how I test depend on my particular problems.</p>
<div class="footnotes">
<ul>
<li id="0_1">[1] This is known as <a href="https://en.wikipedia.org/wiki/data-driven-testing">data-driven-testing</a></li>
</ul>
</div>
</body>
</html>