Automating the Testing Oracle

Steven J Zeil

Last modified: Mar 11, 2014

The Testing Process

Earlier we introduced this model of the basic testing process.

1. Automating the Oracle

Why Automate the Oracle?

1.1 How can oracles be automated?

Output Capture

If we are doing systems/regression tests, the first step towards automation is to capture the output:

If output is to:

Output Capture and Drivers

At the unit and integration test level, we are testing functions and ADT member functions that most often produce data, not files, as their output. That data could be of any type.

How can we capture that output in a fashion that allows automated examination?

1.2 Examining Output

File Tools

Alternatives

Custom oracles

expect

expect is a shell for testing interactive programs.

Key expect Commands

spawn
Launch an interactive program.
send
Send a string to a spawned program, simulating input from a user.
expect
Monitor the output from a spawned program. Expect takes a list of patterns and actions:
pattern1 {action1}
pattern2 {action2}
pattern3 {action3}
   ⋮

and executes the first action whose pattern is matched.

interact
Allow person running expect to interact with spawned program. Takes a similar list of patterns and actions.

Sample Expect Script

Log in to other machine and ignore “authenticity” warnings.

#!/usr/local/bin/expect
set timeout 60 
spawn ssh $argv
while {1} {
  expect {
 
    eof                          {break}
    "The authenticity of host"   {send "yes\r"}
    "password:"                  {send "$password\r"}
    "$argv"                      {break} # assume machine name is in prompt
  }
}
interact
close $spawn_id

Expect: Testing a program

    puts "in test0: $programdir/testsets\n"
catch {
   spawn $programdir/testsets
 
   expect \
    "RESULT: 0" {fail "testsets"} \
    "missing expected element" {fail "testsets"} \
    "contains unexpected element" {fail "testsets"} \
    "does not match" {fail "testsets"} \
    "but not destroyed" {fail "testsets"} \
    {RESULT: 1} \{pass "testsets"} \
    eof {fail "testsets"; puts "eofbsl nl"} \
    timeout {fail "testsets"; puts "timeout\n"} 
}
catch {
    close
    wait
}

1.3 Limitations of Capture-and-Examine Tests

Structured Output

For unit/integration test, output is often a data structure.

Repository Output

For system and high-level unit/integration tests, output may be updates to a database or other repository.

Graphics Output

2. Self-Checking Unit & Integration Tests

First Cut at a Self-Checking Test

Suppose you were testing a SetOfInteger ADT and had to test the add function in isolation, you would need to know how the data was stored in the set and would have to write code to search that storage for a value that you had just added in your test. E.g.,

void testAdd (SetOfInteger aSet)
{
   aSet.add (23);
   bool found = false;
   for (int i = 0; i < aSet.numMembers && !found; ++i)
      found = (aSet[i] == 23);
   assert(found);
}

What’s Good and Bad About This?

void testAdd (SetOfInteger aSet)
{
   aSet.add (23);
   bool found = false;
   for (int i = 0; i < aSet.numMembers && !found; ++i)
      found = (aSet.data[i] == 23);
   assert(found);
}

Better Idea: Test the Public Functions Against Each Other

On the other hand, if you are testing the add and the contains function, you could use the second function to check the results of the first:

void testAdd (SetOfInteger aSet)
{
  aSet.add (23);
  assert (aSet.contains(23));
}

Idiom: Preserve and Compare

void testAdd (SetOfInteger startingSet)
{
  SetOfInteger aSet = startingSet;
  aSet.add (23);
  assert (aSet.contains(23));
  if (startingSet.contains(23))
     assert (aSet.size() == startingSet.size());
  else
     assert (aSet.size() == startingSet.size() + 1);
}

Note that we

More Thorough Tests

void testAdd (SetOfInteger aSet)
{
  for (int i = 0; i < 1000; ++i)
    {
     int x = rand() % 500;
     bool alreadyContained = aSet.contains(x);
     int oldSize = aSet.size();
     aSet.add (23);
     assert (aSet.contains(x));
     if (alreadyContained)
        assert (aSet.size() == oldSize);
     else
        assert (aSet.size() == oldSize + 1);
    }
}

assert() might not be quite what we want

Our use of assert() in these examples has mixed results

3. JUnit Testing

JUnit

JUnit is a testing framework that has seen wide adoption in the Java community and spawned numerous imitations for other programming languages. \bai Introduces a structure for a * suite (separately executable program) of * test cases (functions), * each performing multiple self-checking tests (assertions) * using a richer set of assertion operators

3.1 JUnit Basics

Getting Started: Eclipse & JUnit

Let’s suppose we are building a mailing list application. the mailing list is a collection of contacts.

Contact.java

MailingList.java

A First Test Suite

Right-click on the project and select New ... JUnit Test Case.

A First Test Suite (cont.)

You’ll get something like this:

package mailinglist;

import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;

public class TestMailingList {

    @Test
    public void testMailingList() {
        fail("Not yet implemented");
    }

    @Test
    public void testAddContact() {
        fail("Not yet implemented");
    }

}

Save this, and run it (right-click on the new file in the Package Explorer, and select Run As ... JUnit Test)

A First Test

Let’s implement a test for the mailing list constructor.

/**
 * Create an empty mailing list
 *
 */
public MailingList() {

Glancing through the MailingList interface, what would we expect of an empty mailing list?

Testing the Constructor

Change the testMailingList function to


@Test
public void testMailingList() {
    MailingList ml = new MailingList();  ➊
    assertFalse (ml.contains("Jones"));  ➋
    assertNull (ml.getContact("Smith"));
    assertEquals (0, ml.size());         ➌
}


Running the Constructor Test

Testing addContact

Now let’s add a test for addContact:

After adding a contact

Testing addContact - first pass

Try this test (run it!)

@Test
public void testAddContact() {
    MailingList ml = new MailingList();
    Contact jones = new Contact("Jones", 
                                "21 Penn. Ave.");
    ml.addContact (jones);
    assertTrue (ml.contains("Jones"));
    assertFalse (ml.contains("Smith"));
    assertEquals ("Jones", ml.getContact("Jones").getName());
    assertNull (ml.getContact("Smith"));
    assertEquals (1, ml.size());
}

It works. But it feels awfully limited.

A Diversion - Test Setup

Before we try making that test more rigorous, let’s look at how we can organize the set up of auxiliary data like our contacts:

Contact jones;

@Before
public void setUp() throws Exception {
    jones = new Contact("Jones", "21 Penn. Ave.");
}
  ⋮
@Test
 public void testAddContact() {
     MailingList ml = new MailingList();
     ml.addContact (jones);
        ⋮

Fixtures

Testing addContact - setup

Contact jones;
Contact baker;
Contact holmes;
Contact wolfe;
MailingList mlist;

@Before
public void setUp() throws Exception {
    jones = new Contact("Jones", "21 Penn. Ave.");
    baker = new Contact("Baker", "Drury Ln.");
    holmes = new Contact("Holmes", 
                         "221B Baker St.");
    wolfe = new Contact("Wolfe", 
                        "454 W. 35th St.");
    mlist = MailingList();
    mlist.addContact(baker);
    mlist.addContact (holmed);
    mlist.addContact (baker);
}

Create a series of contacts

Testing addContact - improved

@Test
public void testAddContact() {
    assertTrue (mlist.contains("Jones"));
    assertEquals ("Jones", 
                  mlist.getContact("Jones").getName());
    assertEquals (4, ml.size());
}

Still seems a bit limited - we’ll address that later.

4. C++ Unit Testing

4.1 Google Test

Google Test

Google Test, a.k.a. gtest, provides a similar *Unit environment for C++

Example

This time, the C++ version of the mailing list.

  1. Source code is here.
    • Unpack into a convenient directory.
  2. With Eclipse create a C++ project in that directory.
  3. Compile
  4. Run the resulting executable as a local C++ application
  5. Run as a *unit test:
    • Right-click on the binary executable, select “Run as … Run configurations”.
    • Select C/C++ Unit, click “New” button
    • On the “C/C++ Testing” tab, select Tests Runner: Google Tests Runner
    • Click “Run”

Examining the ADT

mailinglist.h

Should look familiar after the Java version

Examining the Tests

MailingListTests.cpp

Roughly similar to the JUnit tests

4.2 Boost Test Framework

Boost UTF

Boost is a well-respected collection of libraries for C++.

Boost Test

Example

Again, the C++ version of the mailing list.

  1. Source code is here.
    • Unpack into a convenient directory.
  2. With Eclipse create a C++ project in that directory.
    • Add path to Boost include directory
  3. Compile
  4. Run the resulting executable as a local C++ application
  5. Run as a *unit test:
    • Right-click on the binary executable, select “Run as … Run configurations”.
    • Select C/C++ Unit, click “New” button
    • On the “C/C++ Testing” tab, select Tests Runner: Boost Tests Runner
    • Click “Run”

Examining the Tests

MailingListTests2.cpp