Merge lp:~csmith-thecsl/mvhub/abtesting into lp:mvhub
- abtesting
- Merge into trunk_2format
Status: | Needs review |
---|---|
Proposed branch: | lp:~csmith-thecsl/mvhub/abtesting |
Merge into: | lp:mvhub |
Diff against target: |
806 lines (+791/-0) 3 files modified
app-mvhub/setup/database/sql/012_create_usertest_tables.sql (+26/-0) lib-mvhub/lib/MVHub/UserTest.pm (+541/-0) lib-mvhub/t/UserTest/new.t (+224/-0) |
To merge this branch: | bzr merge lp:~csmith-thecsl/mvhub/abtesting |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Dan MacNeil | Needs Fixing | ||
Review via email: mp+81213@code.launchpad.net |
Commit message
Description of the change
Added a module 'MVHub::UserTest' along with database tables 'usertest', 'usertest_tags', in order to perform multivariate or ABTesting.
The module handles the generation of multiple versions of some output and tracks conversions using the database table.
Test cases are tracked using a cookie/session scheme.
Dan MacNeil (omacneil) wrote : | # |
17 + my $mytest = new MVHub::UserTest(
Indirect object syntax is deprecated. See Perl Best Practices, chapter 15, objects pages 349-351
Dan MacNeil (omacneil) wrote : | # |
changes to app-mvhub/
However, it is really, really nice to have sample code.
**optionally***, create a completely artificial example section in POD
- 627. By csmith
-
- Changed the database interaction so that not only conversions are logged, but initiated sessions, and conversions are logged
- Changed database schema to reflect this
- Updated tests
- Got rid of indirect objects
- reverted contact_form.pl to trunk version - 628. By csmith
-
- Fixed use of indirect objects in example code in the pod for UserTest.pm
Dan MacNeil (omacneil) wrote : | # |
Recently recorded standard
http://
Dan MacNeil (omacneil) wrote : | # |
130 +sub generate {
131 + my $self = shift or die q/missing param '$self'/;
139 +sub begin_test {
140 + my $self = shift or die q/missing param '$self'/;
http://
Dan MacNeil (omacneil) wrote : | # |
"assert" means:
"we really don't expect to have a problem"
not:
"routine data validation"
You might rename some _assert* routines to _validate*
Dan MacNeil (omacneil) wrote : | # |
176 + # don't go crazy handling an invalid test id here.
177 + # It could just be that a session expired while the
178 + # test was running (This test data is lost, wave goodbye).
179 + if ( !$usertest_id ) {
180 + return 0;
181 + }
Helpful comment, also points to issue with longer term conversions
Dan MacNeil (omacneil) wrote : | # |
220 + my $cookie_name = 'UTSSID';
possibly better:
220 + my $cookie_name = 'mvhub-UTSSID';
How do we handle multiple simultanious A/B tests ?
"We don't", might be a fine answer.
Dan MacNeil (omacneil) wrote : | # |
462 + cases - Gives an array of names of *well scoped*
What does *Well scoped* mean ?
Is there a simpler, more self-explanatory term ?
Dan MacNeil (omacneil) wrote : | # |
476 + casenames - An array of semantically meaningful names to
477 + use when saving conversions for individual
478 + cases. Defaults to ["A", "B"].
fewer words/same meaning better, specific examples better. Perhaps:
+ casenames - names used saving conversions for individual
+ cases. For example: ["current submit button",'trial big red submit button' ]
+ Defaults to ["A", "B"].
Dan MacNeil (omacneil) wrote : | # |
86 + if ( exists( $args{casenames} ) ) {
87 + @casenames = delete $args{casenames};
88 + }
You are assigning a scalar or an array reference to an array
89 + else {
90 + @casenames = [ "A", "B" ];
91 + }
You are assigning an array reference to an array.
Dan MacNeil (omacneil) wrote : | # |
109 + TESTNAME => $testname,
110 + CASE => -1,
111 + CASENAMES => @casenames,
Assigning an array to a scalar, you probably want \@casenames
112 + MODE => $mode,
113 + CASES => @cases,
Assigning an array to a scalar, you probably want \@casenames
114 + CASECOUNT => $casecount,
115 + PARAMS => {%params}
you should probably create test code that accesses all these things so you know that they are being stored correctly.
Dan MacNeil (omacneil) wrote : | # |
589 +# EDIT
Should remove this line
Dan MacNeil (omacneil) wrote : | # |
590 +use Test::More tests => 20;
591 +use Test::MockModule;
592 +use Test::MockObject;
593 +use Test::Exception;
594 +use CGI;
595 +use MVHub::UserTest;
Dan MacNeil (omacneil) wrote : | # |
165 +# ***ALWAYS REMEMBER TO CALL begin_test BEFORE THE GENERATED
166 +# CASE TEST IS OUTPUTTED***
Not done in SYNOPSIS
Dan MacNeil (omacneil) wrote : | # |
# Maybe some different names for sub routines.
#!/usr/bin/perl
# Example
use strict;
use warnings;
use CGI;
use MVHub::UserTest;
my $TESTNAME = 'red vs blue vs green submit button 2011-12-01';
if ( !MVHub:
MVHub:
name => $TESTNAME,
cases => [ 'red', 'blue', 'green' ]
);
}
my $q = CGI->new();
if ( $q->param('Submit') || ) {
# record_conversion sucks cookie / hidden field / url param
# for user
MVHub:
}
else { # starting test, store coo
# store start of test run in database
my $tr = MVHub::
my $color = $tr->get_
my $cookie = $tr->get_
print $q->header( cookie => [$cookie] );
print generate_
}
sub generate_form {
my $color = shift;
return <<"FORM";
<form>
<input type="submit" name="Submit" value="Submit" style='
<input
</form>
FORM
}
Dan MacNeil (omacneil) wrote : | # |
Existing tables break 2nd Normal form
random, untested , partly formed SQL
BEGIN;
INSERT INTO version_log ( version,note )
VALUES (12,'tables for in AB testing');
CREATE TABLE usertest_tests (
id integer PRIMARY KEY,
name text
);
-- each test can have several cases / choices /outcomes
CREATE TABLE usertest_cases (
id integer PRIMARY KEY,
test_id integer NOT NULL,
FOREIGN KEY (test_id) REFERENCES usertest_tests (id),
name text
);
-- each test can be run many times with many results
CREATE TABLE usertest_runs (
id integer PRIMARY KEY,
test_id integer NOT NULL,
FOREIGN KEY (test_id) REFERENCES usertest_tests (id),
"timestamp" timestamp without time zone DEFAULT now() NOT NULL
);
CREATE TABLE usertest_
run_id integer,
case_id integer,
FOREIGN KEY (case_id) REFERENCES usertest_cases (id),
FOREIGN KEY (run_id) REFERENCES usertest_runs (id),
"timestamp" timestamp without time zone DEFAULT now() NOT NULL
);
COMMIT;
SELECT
usertest_
count(*) AS cnt
FROM
usertest_
GROUP BY usertest_
Unmerged revisions
- 628. By csmith
-
- Fixed use of indirect objects in example code in the pod for UserTest.pm
- 627. By csmith
-
- Changed the database interaction so that not only conversions are logged, but initiated sessions, and conversions are logged
- Changed database schema to reflect this
- Updated tests
- Got rid of indirect objects
- reverted contact_form.pl to trunk version - 626. By csmith
-
- Reverted test_*_db.dql
- Created new schema update file for usertest tables - 625. By csmith
-
- 100% test coverage
- Added the ability to use a custom cookie if desired - 624. By csmith
-
- Added cookie for persistent sessions
- 623. By csmith
-
- Added tests for UserTest module
- 622. By csmith
-
- Finished param() support
- 621. By csmith
-
- Added more input validation to constructor
- Added support for arbitrary numbers of cases in the default random case chooser
- Changed symbolic constants to use the Readonly package - 620. By csmith
-
- ran perltidy
- 619. By csmith
-
- fixed casenames in UserTest module
- fixed wrong increment count in database sequencers
- added conversion calls to contact form test
Preview Diff
1 | === added file 'app-mvhub/setup/database/sql/012_create_usertest_tables.sql' |
2 | --- app-mvhub/setup/database/sql/012_create_usertest_tables.sql 1970-01-01 00:00:00 +0000 |
3 | +++ app-mvhub/setup/database/sql/012_create_usertest_tables.sql 2011-11-07 23:47:27 +0000 |
4 | @@ -0,0 +1,26 @@ |
5 | +--add version number and note before you add the other sql |
6 | +BEGIN; |
7 | +INSERT INTO version_log ( version,note ) |
8 | + VALUES (12,'added the usertest, usertest_tags tables to the database'); |
9 | + |
10 | +CREATE TABLE usertest ( |
11 | + usertest_id integer DEFAULT nextval(('usertest_id_sequence'::text)::regclass) NOT NULL, |
12 | + usertest_testname text, |
13 | + usertest_testcase text, |
14 | + usertest_converted boolean, |
15 | + "timestamp" timestamp without time zone DEFAULT now() NOT NULL |
16 | +); |
17 | + |
18 | +CREATE TABLE usertest_tags ( |
19 | + usertest_id integer, |
20 | + usertest_tag text |
21 | +); |
22 | + |
23 | +CREATE SEQUENCE usertest_id_sequence |
24 | + START WITH 1 |
25 | + INCREMENT BY 1 |
26 | + NO MAXVALUE |
27 | + NO MINVALUE |
28 | + CACHE 1; |
29 | + |
30 | +COMMIT; |
31 | |
32 | === added file 'lib-mvhub/lib/MVHub/UserTest.pm' |
33 | --- lib-mvhub/lib/MVHub/UserTest.pm 1970-01-01 00:00:00 +0000 |
34 | +++ lib-mvhub/lib/MVHub/UserTest.pm 2011-11-07 23:47:27 +0000 |
35 | @@ -0,0 +1,541 @@ |
36 | +# UserTest: A module for testing user interaction with |
37 | +# the MVHub software. |
38 | +# |
39 | +# see perldoc UserTest.pm for more info. |
40 | + |
41 | +package MVHub::UserTest; |
42 | + |
43 | +use strict; |
44 | +use warnings; |
45 | + |
46 | +use CGI; |
47 | +use CGI::Cookie; |
48 | +use CGI::Session; |
49 | +use Readonly; |
50 | + |
51 | +use MVHub::Utils 'assert'; |
52 | + |
53 | +use base 'MVHub::CGIAppBase'; |
54 | + |
55 | +### Constants for giving nice names to the |
56 | +# built in case choosers |
57 | + |
58 | +# Choose random case |
59 | +Readonly our $CASE_RAND => '_case_random'; |
60 | + |
61 | +# Choose first case |
62 | +Readonly our $CASE_A => '_case_A'; |
63 | + |
64 | +# Choose second case |
65 | +Readonly our $CASE_B => '_case_B'; |
66 | + |
67 | +### Creates new UserTest objects |
68 | +sub new { |
69 | + my $class = shift; |
70 | + my %args = @_; |
71 | + my $mode = delete $args{mode}; |
72 | + my $testname = delete $args{testname}; |
73 | + my @casenames = (); |
74 | + my @cases = delete $args{cases}; |
75 | + my %params = (); |
76 | + |
77 | + # defaults |
78 | + if ( !$mode ) { |
79 | + $mode = $CASE_RAND; |
80 | + } |
81 | + |
82 | + if ( exists( $args{params} ) ) { |
83 | + %params = %{ delete $args{params} }; |
84 | + } |
85 | + |
86 | + if ( exists( $args{casenames} ) ) { |
87 | + @casenames = delete $args{casenames}; |
88 | + } |
89 | + else { |
90 | + @casenames = [ "A", "B" ]; |
91 | + } |
92 | + |
93 | + my $casecount = scalar( @{ $cases[0] } ); |
94 | + my $namecount = scalar( @{ $casenames[0] } ); |
95 | + |
96 | + # validation |
97 | + MVHub::Utils::assert( $testname, "UserTest requires a test name." ); |
98 | + MVHub::Utils::assert( $casecount > 1, |
99 | + "UserTest requires at least two test cases." ); |
100 | + MVHub::Utils::assert( $casecount == $namecount, |
101 | + "UserTest received mismatching @casenames,@cases arrays." ); |
102 | + _assert_sub_exists($mode); |
103 | + |
104 | + foreach ( @{ $cases[0] } ) { |
105 | + _assert_sub_exists($_); |
106 | + } |
107 | + |
108 | + my $self = { |
109 | + TESTNAME => $testname, |
110 | + CASE => -1, |
111 | + CASENAMES => @casenames, |
112 | + MODE => $mode, |
113 | + CASES => @cases, |
114 | + CASECOUNT => $casecount, |
115 | + PARAMS => {%params} |
116 | + }; |
117 | + |
118 | + bless( $self, $class ); |
119 | + |
120 | + # since we will need the database to get some |
121 | + # required settings, call cgiapp_init() so the |
122 | + # base class can load the database. |
123 | + $self->cgiapp_init(); |
124 | + |
125 | + return $self; |
126 | +} |
127 | + |
128 | +# Call this when you want to generate the actual |
129 | +# test output (what the end-user sees) |
130 | +sub generate { |
131 | + my $self = shift or die q/missing param '$self'/; |
132 | + |
133 | + # call the selected case subroutine |
134 | + return $self->_call_case_sub( $self->_get_case() ); |
135 | +} |
136 | + |
137 | +# This puts the current instance (session) of the test |
138 | +# into the database. |
139 | +sub begin_test { |
140 | + my $self = shift or die q/missing param '$self'/; |
141 | + my @casenames = @{ $self->{CASENAMES} }; |
142 | + my $session = $self->_get_session(); |
143 | + my $testname = $self->{TESTNAME}; |
144 | + my $case = $self->_get_case(); |
145 | + |
146 | + MVHub::Utils::assert( exists( $casenames[$case] ), |
147 | + "UserTest supplied case chooser generated a bad index." ); |
148 | + |
149 | + $session->param( $testname, |
150 | + $self->_insert_usertest( $testname, $casenames[$case] ) ); |
151 | + |
152 | + return 1; |
153 | +} |
154 | + |
155 | +# Call this when you want to mark a session |
156 | +# as converted. |
157 | +# |
158 | +# IMPORTANT NOTE: |
159 | +# |
160 | +# conversion doesn't check to ensure a valid case was entered into the |
161 | +# for this instance. To do so would require an extra database call. |
162 | +# |
163 | +# To avoid unexpected results: |
164 | +# |
165 | +# ***ALWAYS REMEMBER TO CALL begin_test BEFORE THE GENERATED |
166 | +# CASE TEST IS OUTPUTTED*** |
167 | +# |
168 | +sub conversion { |
169 | + my $self = shift or die q/missing param '$self'/; |
170 | + my $tags = shift; |
171 | + my $dbh = $self->dbh(); |
172 | + my $testname = $self->{TESTNAME}; |
173 | + my $session = $self->_get_session(); |
174 | + my $usertest_id = $session->param($testname); |
175 | + |
176 | + # don't go crazy handling an invalid test id here. |
177 | + # It could just be that a session expired while the |
178 | + # test was running (This test data is lost, wave goodbye). |
179 | + if ( !$usertest_id ) { |
180 | + return 0; |
181 | + } |
182 | + |
183 | + # Mark the session as converted |
184 | + MVHub::Utils::DB::update_one_record( |
185 | + dbh => $dbh, |
186 | + table => 'usertest', |
187 | + values_href => { usertest_converted => 1 }, |
188 | + where_href => { usertest_id => $usertest_id } |
189 | + ); |
190 | + |
191 | + # Put in any tags the user wanted to attach |
192 | + foreach (@$tags) { |
193 | + $self->_insert_usertest_tag( $usertest_id, $_ ); |
194 | + } |
195 | + |
196 | + return 1; |
197 | +} |
198 | + |
199 | +# Gets the value of a param that was passed to |
200 | +# the constructor. |
201 | +sub param { |
202 | + my $self = shift; |
203 | + my $key = shift; |
204 | + my %params = %{ $self->{PARAMS} }; |
205 | + |
206 | + if ( exists( $params{$key} ) ) { |
207 | + return $params{$key}; |
208 | + } |
209 | + |
210 | + return 0; |
211 | +} |
212 | + |
213 | +# Initializaes the session used for tracking the |
214 | +# test data. |
215 | +sub init_session { |
216 | + my $self = shift; |
217 | + my $cookie = shift; |
218 | + my $ssid = undef; |
219 | + my %session_data = (); |
220 | + my $cookie_name = 'UTSSID'; |
221 | + my $cookie_path = '/cgi-bin/mvhub'; |
222 | + |
223 | + if ( $self->{SESSION_DATA} ) { |
224 | + %session_data = %{ $self->{SESSION_DATA} }; |
225 | + } |
226 | + |
227 | + # Check if we don't have to do any work |
228 | + if ( exists( $session_data{COOKIE} ) ) { |
229 | + return $self->{SESSION_DATA}; |
230 | + } |
231 | + |
232 | + # See if the caller wants to manually set the cookie or |
233 | + # find it if it already exists. |
234 | + if ( $cookie and $cookie->value ) { |
235 | + $ssid = $cookie->value; |
236 | + } |
237 | + else { |
238 | + if ($cookie) { |
239 | + $cookie_name = $cookie->name; |
240 | + $cookie_path = $cookie->path; |
241 | + } |
242 | + |
243 | + my %cookies = CGI::Cookie->fetch; |
244 | + |
245 | + if ( exists( $cookies{$cookie_name} ) ) { |
246 | + $ssid = $cookies{$cookie_name}->value; |
247 | + } |
248 | + } |
249 | + |
250 | + # Create the session object to get access to persistent data |
251 | + my $tmp_dir = $self->get_config_param('ABSOLUTE_PATH.tmp_dir'); |
252 | + my $session |
253 | + = CGI::Session->new( undef, $ssid, { Directory => $tmp_dir } ); |
254 | + |
255 | + if ( !$cookie ) { |
256 | + $cookie = CGI::cookie( |
257 | + -name => $cookie_name, |
258 | + -value => $session->id(), |
259 | + -path => $cookie_path |
260 | + ); |
261 | + } |
262 | + |
263 | + return $self->_set_session( $cookie, $session ); |
264 | +} |
265 | + |
266 | +# Generates a case number for this instance |
267 | +# if one does not already exist |
268 | +sub _get_case { |
269 | + my $self = shift; |
270 | + my $case = $self->{CASE}; |
271 | + |
272 | + if ( $case < 0 ) { |
273 | + $case = $self->_call_case_chooser( $self->{MODE} ); |
274 | + $self->{CASE} = $case; |
275 | + } |
276 | + |
277 | + return $case; |
278 | +} |
279 | + |
280 | +sub _set_session { |
281 | + my $self = shift; |
282 | + |
283 | + $self->{SESSION_DATA}{COOKIE} = shift; |
284 | + $self->{SESSION_DATA}{SESSION} = shift; |
285 | + |
286 | + return $self->{SESSION_DATA}; |
287 | +} |
288 | + |
289 | +sub _get_session { |
290 | + my $self = shift; |
291 | + my %smap = %{ $self->init_session() }; |
292 | + return $smap{SESSION}; |
293 | +} |
294 | + |
295 | +sub get_cookie { |
296 | + my $self = shift; |
297 | + my %smap = %{ $self->init_session() }; |
298 | + return $smap{COOKIE}; |
299 | +} |
300 | + |
301 | +# Calls the appropriate case sub given the cases index. |
302 | +# For ABTesting A=0, B=1 |
303 | +sub _call_case_sub { |
304 | + my $self = shift; |
305 | + my $index = shift; |
306 | + my @cases = @{ $self->{CASES} }; |
307 | + |
308 | + MVHub::Utils::assert( exists( $cases[$index] ), |
309 | + "Invalid routine index generated in UserTest::_get_case_ref." ); |
310 | + |
311 | + my $subname = $cases[$index]; |
312 | + my $subref = \&$subname; |
313 | + |
314 | + return &$subref($self); |
315 | +} |
316 | + |
317 | +# Calls the approprate case chooser (see sub _case_... below) |
318 | +sub _call_case_chooser { |
319 | + my $self = shift; |
320 | + my $mode = shift; |
321 | + |
322 | + my $subname = $mode; |
323 | + my $subref = \&$subname; |
324 | + return &$subref($self); |
325 | +} |
326 | + |
327 | +# inserts a usertest record |
328 | +sub _insert_usertest { |
329 | + my $self = shift; |
330 | + my $testname = shift; |
331 | + my $casename = shift; |
332 | + my $dbh = $self->dbh(); |
333 | + |
334 | + my $usertest_id = MVHub::Utils::DB::get_next_in_sequence( $dbh, |
335 | + 'usertest_id_sequence' ); |
336 | + |
337 | + my $values_href = { |
338 | + usertest_id => $usertest_id, |
339 | + usertest_testname => $testname, |
340 | + usertest_testcase => $casename |
341 | + }; |
342 | + |
343 | + MVHub::Utils::DB::insert_one_record( |
344 | + dbh => $dbh, |
345 | + table => 'usertest', |
346 | + values_href => $values_href |
347 | + ); |
348 | + |
349 | + return $usertest_id; |
350 | +} |
351 | + |
352 | +# inserts a usertest tag record |
353 | +sub _insert_usertest_tag { |
354 | + my $self = shift; |
355 | + my $usertest_id = shift; |
356 | + my $tag = shift; |
357 | + my $dbh = $self->dbh(); |
358 | + |
359 | + my $values_href = { |
360 | + usertest_id => $usertest_id, |
361 | + usertest_tag => $tag |
362 | + }; |
363 | + |
364 | + MVHub::Utils::DB::insert_one_record( |
365 | + dbh => $dbh, |
366 | + table => 'usertest_tags', |
367 | + values_href => $values_href |
368 | + ); |
369 | +} |
370 | + |
371 | +sub _assert_sub_exists { |
372 | + my $sub = shift; |
373 | + MVHub::Utils::assert( exists(&$sub), |
374 | + "UserTest received a reference to a non-existent sub: $sub\n" ); |
375 | +} |
376 | + |
377 | +### Case chooser subs: |
378 | +sub _case_random { |
379 | + my $test = shift; |
380 | + my $casecount = $test->{CASECOUNT}; |
381 | + |
382 | + return int( rand($casecount) ); |
383 | +} |
384 | + |
385 | +sub _case_A { |
386 | + return 0; |
387 | +} |
388 | + |
389 | +sub _case_B { |
390 | + return 1; |
391 | +} |
392 | + |
393 | +# end all modules with 1 |
394 | +1; |
395 | +__END__ |
396 | + |
397 | + |
398 | +=head1 NAME |
399 | + |
400 | +UserTest - Concrete class performing multivariate or AB tests. |
401 | + |
402 | +=head1 SYNOPSIS |
403 | + |
404 | + use MVHub::UserTest; |
405 | + |
406 | + sub a_run_method { |
407 | + my $abtest = _my_test(); |
408 | + |
409 | + print $abtest->generate(); |
410 | + } |
411 | + |
412 | + sub an_event_method { |
413 | + my $abtest = _my_test(); |
414 | + |
415 | + $abtest->conversion(['clicked email', 'not logged in']); |
416 | + } |
417 | + |
418 | + sub test_A { |
419 | + my $test = shift; |
420 | + return 'test A: '.$test->param('custom1'); |
421 | + } |
422 | + |
423 | + sub test_B { |
424 | + my $test = shift; |
425 | + return 'test B: '.$test->param('custom1'); |
426 | + } |
427 | + |
428 | + sub _my_test { |
429 | + return MVHub::UserTest->new( |
430 | + testname => 'my_test_name', |
431 | + cases => ['::test_A', '::test_B'], |
432 | + params => { |
433 | + custom1 => '100' |
434 | + } |
435 | + ); |
436 | + } |
437 | + |
438 | +=head1 DESCRIPTION |
439 | + |
440 | + MVHub::UserTest is a class for performing usability tests that can |
441 | + compare arbitrary permutations of an applications output. By |
442 | + default it is configured to perform AB testing, however as many |
443 | + test cases as desired can be used in testing. Additionally the |
444 | + UserTest module supports light-weight tagging of conversions to |
445 | + ensure well described results. |
446 | + |
447 | +=head1 METHODS |
448 | + |
449 | +=head2 new() |
450 | + |
451 | + returns an MVHub::UserTest object |
452 | + |
453 | + This should be passed a hash containing at minimum: |
454 | + |
455 | + testname - Gives an overall name of the test to be run. |
456 | + testnames refer to test objects and should |
457 | + be logically grouped around a single unit |
458 | + test (ie. using a single test name for testing |
459 | + two different buttons is an abuse of the |
460 | + design. |
461 | + |
462 | + cases - Gives an array of names of *well scoped* |
463 | + subroutines that return the output for |
464 | + a given case index (A = 0, B = 1, etc.). |
465 | + |
466 | + Additionally, the following optional parameters are accepted: |
467 | + |
468 | + mode - The name of a subroutine to use for generating |
469 | + (choosing) a test case index during the generate |
470 | + call. Defaults to MVHub::UserTest::CASE_RAND |
471 | + which chooses a random index. Also provided |
472 | + are MVHUB::UserTest::CASE_A which returns index |
473 | + 0, and MVHUB::UserTest::CASE_B which returns |
474 | + index 1. |
475 | + |
476 | + casenames - An array of semantically meaningful names to |
477 | + use when saving conversions for individual |
478 | + cases. Defaults to ["A", "B"]. |
479 | + |
480 | + params - A hash of optional paramaters to pass along |
481 | + to your case subroutines (a-la CGIApplication). |
482 | + |
483 | +=head2 generate() |
484 | + |
485 | + Generates a test case based on the settings that were used |
486 | + when the test was created. |
487 | + |
488 | +=head2 begin_test() |
489 | + |
490 | + Marks the beginning of a test session (instance) by inserting |
491 | + a row in the database for the test instance. |
492 | + |
493 | + This can be called before or after generate(). |
494 | + ***FAILING TO CALL THIS WILL RESULT IN BAD RESULTS*** |
495 | + |
496 | +=head2 init_session($optional_cookie) |
497 | + |
498 | + Returns a hash containing a session and cookie object. |
499 | + |
500 | + ***YOU MUST OUTPUT THE COOKIE IN THE GENERATION PAGE |
501 | + HEADER TO ENSURE VALID RESULTS!!!*** |
502 | + |
503 | +=head2 get_cookie() |
504 | + |
505 | + Returns a cookie object to embed in a header. |
506 | + |
507 | +=head2 conversion($tags) |
508 | + |
509 | + Marks a user test as a 'conversion' in the database. |
510 | + |
511 | + Optional tags may be provided to provide more semantic data. |
512 | + |
513 | +=head2 param($key) |
514 | + |
515 | + Gets the value of a parameter passed in the constructor. |
516 | + |
517 | +=head1 EXAMPLE CODE |
518 | + |
519 | + # the following code is based off of contact_form.pl |
520 | + |
521 | + my $mytest = MVHub::UserTest->new( |
522 | + testname => 'form_text_test', |
523 | + cases => [ '::formtext_A', '::formtext_B' ], |
524 | + params => { |
525 | + testA => 'test A', |
526 | + testB => 'test B', |
527 | + } |
528 | + ); |
529 | + |
530 | + my $form = CGI::FormBuilder->new( |
531 | + template => 'contact_form.tmpl', |
532 | + method => 'POST', |
533 | + query => $cgi, |
534 | + javascript => 0 |
535 | + ); |
536 | + |
537 | + $form->field( |
538 | + name => 'name', |
539 | + size => 45, |
540 | + label => $mytest->generate(), |
541 | + required => 1 |
542 | + ); |
543 | + |
544 | + if ( $form->submitted && $form->validate ) { |
545 | + |
546 | + $mytest->conversion( [ 'clicked_submit', 'validated' ] ); |
547 | + } |
548 | + else { |
549 | + if ( $form->submitted ) { |
550 | + $mytest->conversion( ['clicked_submit'] ); |
551 | + } |
552 | + |
553 | + $mytest->begin_test(); |
554 | + print CGI::header( -cookie => $cookie ), $form->render; |
555 | + } |
556 | + |
557 | + sub formtext_A { |
558 | + my $testobject = shift; |
559 | + return 'You got ' . $testobject->param('testA'); |
560 | + } |
561 | + |
562 | + sub formtext_B { |
563 | + my $testobject = shift; |
564 | + return 'You got ' . $testobject->param('testB'); |
565 | + } |
566 | + |
567 | +=head1 LICENSE |
568 | + |
569 | +The GNU Affero General Public License, version 3.0 or later |
570 | + |
571 | +=head1 COPYRIGHT |
572 | + |
573 | +Community Software Lab, Inc ( http://thecsl.org ) |
574 | + |
575 | +=cut |
576 | + |
577 | |
578 | === added directory 'lib-mvhub/t/UserTest' |
579 | === added file 'lib-mvhub/t/UserTest/new.t' |
580 | --- lib-mvhub/t/UserTest/new.t 1970-01-01 00:00:00 +0000 |
581 | +++ lib-mvhub/t/UserTest/new.t 2011-11-07 23:47:27 +0000 |
582 | @@ -0,0 +1,224 @@ |
583 | +#!/usr/bin/perl -w |
584 | + |
585 | +use strict; |
586 | +use warnings; |
587 | +use Test::NoWarnings; |
588 | + |
589 | +# EDIT |
590 | +use Test::More tests => 20; |
591 | +use Test::MockModule; |
592 | +use Test::MockObject; |
593 | +use Test::Exception; |
594 | +use CGI; |
595 | +use MVHub::UserTest; |
596 | + |
597 | +# return ->run() output instead of sending to stdout |
598 | +$ENV{CGI_APP_RETURN_ONLY} = 1; |
599 | + |
600 | +my @methods = qw/ |
601 | + generate |
602 | + begin_test |
603 | + conversion |
604 | + init_session |
605 | + param |
606 | + get_cookie |
607 | + _set_session |
608 | + _get_session |
609 | + _get_case |
610 | + _call_case_sub |
611 | + _call_case_chooser |
612 | + _insert_usertest |
613 | + _insert_usertest_tag |
614 | + _assert_sub_exists |
615 | + _case_random |
616 | + _case_A |
617 | + _case_B |
618 | + /; |
619 | + |
620 | +my $test_name; |
621 | +my $result = 1; |
622 | + |
623 | +# ensure no actual database changes are made |
624 | +my $mock_db = Test::MockModule->new('MVHub::Utils::DB'); |
625 | +$mock_db->mock( 'insert_one_record', sub { return 1; } ); |
626 | +$mock_db->mock( 'update_one_record', sub { return 1; } ); |
627 | + |
628 | +### |
629 | +$test_name = 'can run all methods of UserTest class'; |
630 | +### |
631 | +can_ok( 'MVHub::UserTest', @methods ); |
632 | + |
633 | +### |
634 | +$test_name = 'can create new UserTest: basic AB Test'; |
635 | +### |
636 | +my $object = MVHub::UserTest->new( |
637 | + testname => 'sample_ab_test1', |
638 | + cases => [ '::testfunc1', '::testfunc2' ] |
639 | +); |
640 | +isa_ok( $object, 'MVHub::UserTest', $test_name ); |
641 | + |
642 | +### |
643 | +$test_name |
644 | + = 'can create new UserTest: create multivariate test with all options'; |
645 | +### |
646 | +my $mock_cookie = Test::MockObject->new(); |
647 | +my $mock_session = Test::MockObject->new(); |
648 | +$object = MVHub::UserTest->new( |
649 | + testname => 'sample_multi_test3', |
650 | + cases => [ '::testfunc1', '::testfunc2', '::testfunc3' ], |
651 | + mode => '::testmode', |
652 | + casenames => [ 'Case 1', 'Case 2', 'Case 3' ], |
653 | + params => { |
654 | + testparam1 => 'hello', |
655 | + testparam2 => 'world', |
656 | + testparam3 => 1234 |
657 | + } |
658 | +); |
659 | +isa_ok( $object, 'MVHub::UserTest', $test_name ); |
660 | + |
661 | +$mock_cookie->mock( 'value', sub { return 0; } ); |
662 | +$mock_cookie->mock( 'name', sub {'test'} ); |
663 | +$mock_cookie->mock( 'path', sub {'test'} ); |
664 | +$mock_cookie->mock( 'value', sub { return 0; } ); |
665 | +$mock_session->mock( 'param', sub { return 1; } ); |
666 | +$mock_session->mock( 'id', sub { return 0; } ); |
667 | + |
668 | +$object->init_session(); |
669 | +$object->_set_session( $mock_cookie, $mock_session ); |
670 | + |
671 | +### |
672 | +$test_name = 'can access parameters'; |
673 | +### |
674 | + |
675 | +ok( $object->param('testparam3') eq 1234, $test_name ); |
676 | + |
677 | +### |
678 | +$test_name = 'can gracefully handle missing params'; |
679 | +### |
680 | + |
681 | +ok( $object->param('testparam4') eq 0, $test_name ); |
682 | + |
683 | +### |
684 | +$test_name = 'can get valid cookie'; |
685 | +### |
686 | + |
687 | +ok( $object->get_cookie(), $test_name ); |
688 | + |
689 | +### |
690 | +$test_name = 'can call case chooser MVHub::UserTest::$CASE_RAND'; |
691 | +### |
692 | + |
693 | +my $case = $object->_call_case_chooser($MVHub::UserTest::CASE_RAND); |
694 | +ok( ( $case ge 0 and $case le 2 ), $test_name ); |
695 | + |
696 | +### |
697 | +$test_name = 'can call case chooser MVHub::UserTest::$CASE_A'; |
698 | +### |
699 | + |
700 | +$case = $object->_call_case_chooser($MVHub::UserTest::CASE_A); |
701 | +ok( $case eq 0, $test_name ); |
702 | + |
703 | +### |
704 | +$test_name = 'can call case chooser MVHub::UserTest::$CASE_B'; |
705 | +### |
706 | + |
707 | +$case = $object->_call_case_chooser($MVHub::UserTest::CASE_B); |
708 | +ok( $case eq 1, $test_name ); |
709 | + |
710 | +### |
711 | +$test_name = 'can create an instance with begin_test'; |
712 | +### |
713 | + |
714 | +$object->{CASE} = -1; |
715 | +ok( $object->begin_test(), $test_name ); |
716 | + |
717 | +### |
718 | +$test_name = 'can generate a test element'; |
719 | +### |
720 | + |
721 | +ok( $object->generate(), $test_name ); |
722 | + |
723 | +### |
724 | +$test_name = 'can mark a session converted'; |
725 | +### |
726 | + |
727 | +ok( $object->conversion( [ 'test1', 'test2' ] ), $test_name ); |
728 | + |
729 | +### |
730 | +$test_name = 'can handle bad param on generate'; |
731 | +### |
732 | + |
733 | +dies_ok( sub { MVHub::UserTest::generate(); }, $test_name ); |
734 | + |
735 | +### |
736 | +$test_name = 'can handle bad param on begin_test'; |
737 | +### |
738 | + |
739 | +dies_ok( sub { MVHub::UserTest::begin_test(); }, $test_name ); |
740 | + |
741 | +### |
742 | +$test_name = 'can handle bad param on conversion'; |
743 | +### |
744 | + |
745 | +dies_ok( sub { MVHub::UserTest::conversion(); }, $test_name ); |
746 | + |
747 | +### |
748 | +$test_name = 'can handle invalid instance id in session'; |
749 | +### |
750 | +$object = MVHub::UserTest->new( |
751 | + testname => 'sample_test_4', |
752 | + cases => [ '::testfunc1', '::testfunc2' ] |
753 | +); |
754 | +$mock_session->set_always( 'param', 0 ); |
755 | +$object->init_session(); |
756 | +$object->_set_session( $mock_cookie, $mock_session ); |
757 | + |
758 | +ok( $object->conversion() eq 0, $test_name ); |
759 | + |
760 | +### |
761 | +$test_name = 'can pass initialized cookie in init_session'; |
762 | +### |
763 | +$object = MVHub::UserTest->new( |
764 | + testname => 'sample_test_4', |
765 | + cases => [ '::testfunc1', '::testfunc2' ] |
766 | +); |
767 | +$mock_cookie->set_always( 'value', 100 ); |
768 | +ok( $object->init_session($mock_cookie), $test_name ); |
769 | + |
770 | +### |
771 | +$test_name = 'can pass unitialized cookie in init_session'; |
772 | +### |
773 | +$object = MVHub::UserTest->new( |
774 | + testname => 'sample_test_4', |
775 | + cases => [ '::testfunc1', '::testfunc2' ] |
776 | +); |
777 | +$mock_cookie->set_always( 'value', 0 ); |
778 | +ok( $object->init_session($mock_cookie), $test_name ); |
779 | + |
780 | +### |
781 | +$test_name = 'can get cookie value'; |
782 | +### |
783 | +my $mock_cookies = Test::MockModule->new('CGI::Cookie'); |
784 | +$mock_cookies->mock( 'fetch', sub { return %{ { test => $mock_cookie } }; } ); |
785 | + |
786 | +$object = MVHub::UserTest->new( |
787 | + testname => 'sample_test_4', |
788 | + cases => [ '::testfunc1', '::testfunc2' ] |
789 | +); |
790 | +ok( $object->init_session($mock_cookie), $test_name ); |
791 | + |
792 | +sub testmode { |
793 | + return 1; |
794 | +} |
795 | + |
796 | +sub testfunc1 { |
797 | + return '1'; |
798 | +} |
799 | + |
800 | +sub testfunc2 { |
801 | + return '2'; |
802 | +} |
803 | + |
804 | +sub testfunc3 { |
805 | + return '3'; |
806 | +} |
381 + MVHub:: Utils:: DB::insert_ one_record(
Good, most people would have created their own sub routine to do this. It is nice you took the time to read the existing docs/code