views:

99

answers:

2

Hi, I'm trying to test some code in different situations (for different result sets). I've got the first test running well, but the next one is trying to reuse the first "table".

My result sets:

my $usernames_many = [
      { username => '1234567' },
      { username => '2345678' },
   ];
my $usernames_empty = [
   ];

but now when I try these calls:

$mock_dbi->set_retval_scalar(MOCKDBI_WILDCARD, "SELECT username FROM location", $usernames_many);
is_deeply(find_multiple_registrations($mock_db, 15), [ '1234567', '2345678' ], "many entries");

$mock_dbi->set_retval_scalar(MOCKDBI_WILDCARD, "SELECT username FROM location", $usernames_empty);
is_deeply(find_multiple_registrations($mock_db, 15), [ ], "no entries");

The first test passes, but the second one results in:

not ok 3 - no entries
#   Failed test 'no entries'
#   at ./report_many_registrations_test.pl line 28.
#     Structures begin differing at:
#          $got->[0] = '1234567'
#     $expected->[0] = Does not exist

Which seems to indicate the first resultset was used again instead. How can I clean a resultset? Or reset the state in some other way?

+1  A: 

If you (are able to) change the second test to something like:

$mock_dbi->set_retval_scalar( 
    MOCKDBI_WILDCARD, 
    "Get me username stuff",   # <= something different
    $usernames_empty
);

then you may find that the test now works.

This is because Test::MockDBI only uses the SQL text provided has a placeholder for which it returns the DBI object after a matching dbi->prepare( 'Get me username stuff' );

Update - Here is a workaround that doesn't require changing the SQL:

BEGIN { push @ARGV, "--dbitest=1"; }

use 5.012;
use warnings;
use Test::More;
use Test::MockDBI ':all';

my $mock_dbi = Test::MockDBI::get_instance;
my $dbh      = DBI->connect(q{}, q{}, q{});
my $sql      = 'SELECT username FROM location';

my $Data =  [
    { username => '1234567' },
    { username => '2345678' },
];

$mock_dbi->set_retval_scalar( MOCKDBI_WILDCARD, $sql, sub { $Data } );

is_deeply( get_mock_user($dbh, $sql), [1234567,2345678], 'many entries' );

$Data = [];  # change the data!

is_deeply( get_mock_user($dbh, $sql), [], 'no entries' );

done_testing;

sub get_mock_user {
    my ($dbh, $sql) = @_;
    $dbh->prepare( $sql );
    [ map { $_->{username} } @{ $dbh->fetchrow_arrayref } ];
}

/I3az/

draegtun
What about tests which run the same query then? I can't really change the query to some random string in the real code :)
viraptor
@viraptor: Just change it in your tests (probably by slipping in an abstraction).
draegtun
BTW: In case it helps here the tests I used to "replicate" and then "get round" the problem: http://gist.github.com/540782
draegtun
I'd be happy with that in new code, but I've got a large, live codebase which cannot be easily rewritten this way. I'd really like to not modify it at all. Essentially, it's not always the case that I'm calling the function directly. Let's assume here, that I've got 100 situations like this one and each is called from 100 different functions, so it's simply not practical to rewrite them. (To refactor I'd need to have tests, which I'm creating, but until I have tests, I cannot refactor ;) )
viraptor
Also this method is not practical if I have 3 levels deep call stack - I'd have to pass every query string from the main level up.
viraptor
+1  A: 

The implementation of set_retval_scalar may at first appear discouraging:

sub set_retval_scalar {
    my $self   = shift;                 # my blessed self
    my $type   = shift;                 # type number from --dbitest=TYPE
    my $sql    = shift;                 # SQL pattern for badness

    push @{ $scalar_retval{$type} },
     { "SQL" => $sql, "retval" => $_[0] };
}

The reason the first resultset appeared to be used again is successive calls to set_retval_scalar are cumulative. After the second call to set_retval_scalar, just before the second test, the internal bookkeeping for Test::MockDBI resembles

[ # first resultset
  { SQL => "SELECT username ...",
    retval => [{ username => '1234567' }, ...]
  },
  # second resultset
  { SQL => "SELECT username ...",
    retval => []
  }
]

Under the hood when your second test queries SELECT username ..., _force_retval_scalar in Test::MockDBI searches this data structure for the currently executing query and stops on the first hit it finds. Both resultsets are associated with the same query, so the second doesn't have a chance to match.

But there's hope! Notice that set_retval_scalar copies only the outermost reference—a reference to an array that you control!

Modify your test slightly:

my @usernames_many = (
  { username => '1234567' },
  { username => '2345678' },
);

my @usernames_empty = ();

my $usernames = [];
$mock_dbi->set_retval_scalar(
  MOCKDBI_WILDCARD,
  "SELECT username FROM location",
  $usernames);

With this fixture, you need only change the contents of @$usernames (that is, the array referred to by $usernames) to change the canned result of the query:

@$usernames = @usernames_many;
is_deeply(find_multiple_registrations($mock_db, 15),
          [ '1234567', '2345678' ],
          "many entries");

@$usernames = @usernames_empty;
is_deeply(find_multiple_registrations($mock_db, 15),
          [ ],
          "no entries");

With these modifications, both tests pass.

IMPORTANT: Always assign to @$usernames! You may be tempted to save a few keystrokes by writing

$usernames = [];  # empty usernames
is_deeply(find_multiple_registrations($mock_db, 15),
          [ ],
          "no entries");

but this will cause your test to fail for nearly the same reason as the test from your question: the fixture will continue to have the same reference that you gave it in the call to set_retval_scalar. Doing it this way would be both incorrect and misleading, a nasty combination.


For completeness, a full working example is below.

#! /usr/bin/perl

use warnings;
use strict;

BEGIN { push @ARGV, "--dbitest" }

use Test::MockDBI qw/ :all /;
use Test::More tests => 2;

my @usernames_many = (
      { username => '1234567' },
      { username => '2345678' },
   );
my @usernames_empty = ();

my $usernames = [];

my $mock_dbi = get_instance Test::MockDBI;
my $mock_db = DBI->connect("dbi:SQLite:dbname=:memory:", "", "");
$mock_db->{RaiseError} = 1;
$mock_db->do(q{CREATE TABLE location (username char(10))});

sub find_multiple_registrations {
  my($dbh,$limit) = @_;
  my $sth = $dbh->prepare("SELECT username FROM location");
  $sth->execute;
  [ map $_->{username} => @{ $sth->fetchall_arrayref } ];
}

$mock_dbi->set_retval_scalar(
  MOCKDBI_WILDCARD,
  "SELECT username FROM location",
  $usernames);

@$usernames = @usernames_many;
is_deeply(find_multiple_registrations($mock_db, 15),
          [ '1234567', '2345678' ],
          "many entries");

@$usernames = ();
is_deeply(find_multiple_registrations($mock_db, 15),
          [ ],
          "no entries");

Output:

1..2

connect() 'CONNECT TO dbi:SQLite:dbname=:memory: AS  WITH '

do() 'CREATE TABLE location (username char(10))'

prepare() 'SELECT username FROM location'

execute()

fetchall_arrayref()
ok 1 - many entries

prepare() 'SELECT username FROM location'

execute()

fetchall_arrayref()
ok 2 - no entries
Greg Bacon