Merge lp:~csmith-thecsl/mvhub/abtesting into lp:mvhub

Proposed by csmith
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
Reviewer Review Type Date Requested Status
Dan MacNeil Needs Fixing
Review via email: mp+81213@code.launchpad.net

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.

To post a comment you must log in.
lp:~csmith-thecsl/mvhub/abtesting updated
626. By csmith

- Reverted test_*_db.dql
- Created new schema update file for usertest tables

Revision history for this message
Dan MacNeil (omacneil) wrote :

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

Revision history for this message
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

review: Needs Fixing
Revision history for this message
Dan MacNeil (omacneil) wrote :

changes to app-mvhub/DocumentRoot/cgi-bin/mvhub/contact_form.pl should be reverted as temp code shouldn't go in production. Trunk should always be ready for production.

However, it is really, really nice to have sample code.

**optionally***, create a completely artificial example section in POD

lp:~csmith-thecsl/mvhub/abtesting updated
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

Revision history for this message
Dan MacNeil (omacneil) wrote :
Revision history for this message
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://wiki.thecsl.org/mediawiki/index.php/Standards:Code_style_guide#Database_.2F_SQL_.2F_Schema

Revision history for this message
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*

Revision history for this message
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

Revision history for this message
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.

Revision history for this message
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 ?

Revision history for this message
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"].

Revision history for this message
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.

review: Needs Fixing
Revision history for this message
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.

review: Needs Fixing
Revision history for this message
Dan MacNeil (omacneil) wrote :

589 +# EDIT

Should remove this line

Revision history for this message
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;

http://wiki.thecsl.org/mediawiki/index.php/Standards:Code_style_guide#use_.3Clibrary.3E_statements_ordered_by_type_.283rd_party_vs_ours.29_and_then_alphabetical

review: Needs Fixing
Revision history for this message
Dan MacNeil (omacneil) wrote :

165 +# ***ALWAYS REMEMBER TO CALL begin_test BEFORE THE GENERATED
166 +# CASE TEST IS OUTPUTTED***

Not done in SYNOPSIS

Revision history for this message
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::UserTest->test_exists($TESTNAME) ) {
    MVHub::UserTest->create(
        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::UserTest->record_conversion($q);
}
else { # starting test, store coo

    # store start of test run in database
    my $tr = MVHub::UserTest->new_test_run($TESTNAME);
    my $color = $tr->get_casename();
    my $cookie = $tr->get_case_cookie();
    print $q->header( cookie => [$cookie] );
    print generate_form($color);
}

sub generate_form {
    my $color = shift;
    return <<"FORM";
<form>
      <input type="submit" name="Submit" value="Submit" style='background-color: $color;' />
  <input
</form>
FORM
}

Revision history for this message
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_conversions (
    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_conversions.case_id,
  count(*) AS cnt
FROM
   usertest_conversions
GROUP BY usertest_conversions.case_id;

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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+}

Subscribers

People subscribed via source and target branches