Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing real world applications

Real world?

Why test?

  • Business Value

How to test?

  • Manual - exploratory
  • Automated - regression

Manual Tests (exploratory tests)

  • New feature is working
  • Bug was eliminated
  • General view of the system

Automated Tests (regression tests)

  • Avoid regression
  • Better Software Design (TDD)
  • Your Sanity

Two cases of Automated tests

  • TDD
  • Real world

Real World

  • Application mostly works
  • Huge system
  • Interdependent code
  • Long functions
  • It is hard to test parts of the system

Testing modes

  • Unit testing
  • Integration testing
  • Acceptance testing (BDD ?)

Testing Environment

  • Git
  • Virtualization (Docker? VirtualBox?)
  • One-click and fast setup

Setup - Fixture

  • Web server
  • Databases
  • Windows machines
  • Other devices
  • External services

Fake the world

Test Double

Test Doubles

  • Dummy
  • Fake
  • Stubs
  • Spies
  • Mocks

Mocking what?

  • User created functions (or classes)
  • 3rd party functions (or classes)
  • Functions that are part of the language
  • System calls (STDIO, files, time, etc.)
  • Whole fiilesystem
  • Database
  • Network access
  • External device/service

Perl

Fake (temporary) filesytem

Use temporary directories and files - locate pathes that are hard-coded in the code and replace them with configuration options from a configuration file or environment variables. File::Temp

use File::Temp qw(tempdir);
my $dir = tempdir( CLEANUP => 1 );

Fake library

Have a private implementation of the used library and make sure this one gets loaded instead of the real class. (e.g. by tweaking @INC and loading the module early.

package WWW::Mechanize;
use strict;
use warnings;

our $VERSION = 'fake';

sub new {
    return bless {}, shift;
}

sub get {
    ...
}

1;

Use the real library

use strict;
use warnings;
use Test::More;

plan tests => 1;

use WWW::Mechanize;
diag $WWW::Mechanize::VERSION;
like $WWW::Mechanize::VERSION, qr/^\d+\.\d+$/;

Use the fake library

use strict;
use warnings;
use Test::More;

plan tests => 1;

use Cwd qw(abs_path);
use File::Basename qw(dirname);
use lib dirname(abs_path($0)) . '/lib';

use WWW::Mechanize;
diag $WWW::Mechanize::VERSION;
is $WWW::Mechanize::VERSION, 'fake';

Mocking IO - module

package MyModule1;
use strict;
use warnings;

sub game {
    print("Enter your name: ");
    my $name = <STDIN>;
    chomp $name;
    print("Hello '$name'\n");

    return;
}

42;

Mocking IO - test

use strict;
use warnings;
use Test::More;

use MyModule1;
plan tests => 1;

{
    my $stdout;
    open my $out_fh, '>', \$stdout or die "Cannot open STDOUT to write to string: $!";
    my $stdin = "qwert\n";
    open my $in_fh, '<', \$stdin or die "Cannot open STDIN to read from string: $!";
    local *STDIN = $in_fh;
    local *STDOUT = $out_fh;

    MyModule1::game();

    is($stdout, "Enter your name: Hello 'qwert'\n");
}

Mocking IO

package MyModule2;
use strict;
use warnings;

sub game {
    print("Enter your name: ");
    my $name = <STDIN>;
    chomp $name;
    print("Hello '$name'\n");
    print("Guess a number: ");
    my $number = <STDIN>;
    chomp $number;
    print("Your number '$number' is good\n");
    
    return;
}

42;

Mocking IO - test

use strict;
use warnings;
use Test::More;

use MyModule2;
plan tests => 1;

{
    my $stdout;
    open my $out_fh, '>', \$stdout or die "Cannot open STDOUT to write to string: $!";
    my $stdin = join('',
        "qwert\n",
        "42\n",
    );
    open my $in_fh, '<', \$stdin or die "Cannot open STDIN to read from string: $!";
    local *STDIN = $in_fh;
    local *STDOUT = $out_fh;
    
    MyModule2::game();
    my @expected_stdout = (
        "Enter your name: ",
        "Hello 'qwert'\n",
        "Guess a number: ",
        "Your number '42' is good\n",
    );
    is($stdout, join('',@expected_stdout));
}

Mocking IO - script

use strict;
use warnings;

print("Enter your name: ");
my $name = <STDIN>;
chomp $name;
print("Hello '$name'\n");

Mocking IO - test script

use strict;
use warnings;
use Test::More;


plan tests => 1;

{
    my $stdout;
    open my $out_fh, '>', \$stdout or die "Cannot open STDOUT to write to string: $!";
    my $stdin = "abc def\n";
    open my $in_fh, '<', \$stdin or die "Cannot open STDIN to read from string: $!";
    local *STDIN = $in_fh;
    local *STDOUT = $out_fh;

    do "game.pl";

    is($stdout, "Enter your name: Hello 'abc def'\n");
}

Mocking function of web access

  • Test::Mock::Simple
  • [Mocking function to fake environment](https://perlmaven.com/mocking-function-to-fake-environme.t" %}
package MyWebAPI;
use strict;
use warnings;

use LWP::Simple qw(get);

my $URL = 'http://www.dailymail.co.uk/';

sub new {
    return bless {}, shift;
}

sub count_strings {
    my ($self, @strings) = @_;

    my $content = get $URL;

    my %data;
    foreach my $str (@strings) {
        $data{$str} = () = $content =~ /$str/ig;
    }
    return \%data;
}

1;

Test live web server

use strict;
use warnings;

use FindBin qw($Bin);
use lib $Bin;

use Test::More;
plan tests => 1;

use MyWebAPI;

my $w = MyWebAPI->new;

is_deeply $w->count_strings('Beyonce', 'Miley Cyrus'), 
    {
        'Beyonce'     => 26,
        'Miley Cyrus' => 3,
    };
1..1
not ok 1
#   Failed test at webapi.t line 14.
#     Structures begin differing at:
#          $got->{Miley Cyrus} = '4'
#     $expected->{Miley Cyrus} = '3'
# Looks like you failed 1 test of 1.

Mocking the get method

use strict;
use warnings;

use FindBin qw($Bin);
use lib $Bin;

use Test::More;
plan tests => 1;

use MyWebAPI;

use Test::Mock::Simple;
my $mock = Test::Mock::Simple->new(module => 'MyWebAPI');
$mock->add(get => sub {
    return 'Beyonce Beyonce Miley Cyrus';
});

my $w = MyWebAPI->new;

is_deeply $w->count_strings('Beyonce', 'Miley Cyrus'), 
    {
        'Beyonce'     => 2,
        'Miley Cyrus' => 1,
    };
1..1
ok 1

More test cases

use strict;
use warnings;

use FindBin qw($Bin);
use lib $Bin;

use Test::More;
plan tests => 3;

use MyWebAPI;

use Test::Mock::Simple;
my $mock = Test::Mock::Simple->new(module => 'MyWebAPI');
$mock->add(get => sub {
    return 'Beyonce Beyonce Miley Cyrus';
});

my $w = MyWebAPI->new;

is_deeply $w->count_strings('Beyonce', 'Miley Cyrus'), 
    {
        'Beyonce'     => 2,
        'Miley Cyrus' => 1,
    };

$mock->add(get => sub {
    return 'Beyonce';
});
is_deeply $w->count_strings('Beyonce', 'Miley Cyrus'), 
    {
        Beyonce => 1,
        'Miley Cyrus' => 0,
    };

$mock->add(get => sub {
    return '';
});
is_deeply $w->count_strings('Beyonce', 'Miley Cyrus'), 
    {
        Beyonce => 0,
        'Miley Cyrus' => 0,
    };

Test exception

$mock->add(get => sub {
    die 'Something went wrong'; 
});
is_deeply $w->count_strings('Beyonce', 'Miley Cyrus'), 
    {
        Beyonce => 0,
        'Miley Cyrus' => 0,
    };
1..2
ok 1
Something went wrong at exception.t line 25.
# Looks like you planned 2 tests but ran 1.
# Looks like your test exited with 255 just after 1.

Linewrap bug

$mock->add(get => sub {
    return 'Beyonce Miley Cyrus Miley
Cyrus';
});
is_deeply $w->count_strings('Beyonce', 'Miley Cyrus'), 
    {
        Beyonce => 1,
        'Miley Cyrus' => 2,
    };
1..4
ok 1
ok 2
ok 3
not ok 4
#   Failed test at linewrap.t line 46.
#     Structures begin differing at:
#          $got->{Miley Cyrus} = '1'
#     $expected->{Miley Cyrus} = '2'
# Looks like you failed 1 test of 4.

Mocking time: the session module

package MySession;
use strict;
use warnings;

my %SESSION;
my $TIMEOUT = 60;

sub new {
    return bless {}, shift;
}

sub login {
    my ($self, $username, $pw) = @_;
    # ...
    $SESSION{$username} = time;
    return;
}

sub logged_in {
    my ($self, $username) = @_;
    if ($SESSION{$username} and time - $SESSION{$username} < $TIMEOUT) {
        return 1;
    }
    return 0;
}

1;

Test session timeout

Takes 61 seconds to run...

use strict;
use warnings;

use FindBin qw($Bin);
use lib $Bin;

use Test::More;
plan tests => 3;

use MySession;

my $s = MySession->new;
$s->login('foo', 'secret');
ok $s->logged_in('foo'),  'foo logged in';
ok !$s->logged_in('bar'), 'bar not logged in';
sleep 61;
ok !$s->logged_in('foo'),  'foo not logged in - timeout';

Test session timeout faking the time

use strict;
use warnings;

use FindBin qw($Bin);
use lib $Bin;

use Test::MockTime qw(set_relative_time);
use Test::More;
plan tests => 3;

use MySession;

my $s = MySession->new;
$s->login('foo', 'secret');
ok $s->logged_in('foo'),  'foo logged in';
ok !$s->logged_in('bar'), 'bar not logged in';
set_relative_time(61);
ok !$s->logged_in('foo'),  'foo not logged in - timeout';

Testing a database driven application

  • Have a set of test data (small set of data)
  • Have a copy of the production data
  • Use an in-memory database for testing to make it much faster than a disk-based one.
  • Mock the database access
  • DBD::Mock
  • DBI::Mock

Build Schema by code

Have scripts that can create the database schema and load the initial data and scripts for migration among schema versions.

Manual Schema change

Have DBAs (or developers) change the schema manually on the development and later on the production server. As a minimum have some code that will regularly dump the schema and add it to some version control system.

Mocking in Java

How to move forward?

  • Hard to justify as it usually does not find (new) bugs.
  • Write tests for new features
  • Write tests for bugs reported

How to find bugs?

  • Use a linter.
  • Turn on stricter compilation (and runtime) warnings.

How to find bugs in Perl?

use strict;
use warnings;

Run Perl::Critic on the code.

Testing culture

  • Automated tests are like insurance.
  • Get management support
  • Build a culture