Automating the Testing Oracle

Steven J Zeil

Last modified: Mar 11, 2014

Contents:
1. Automating the Oracle
1.1 How can oracles be automated?
1.2 Examining Output
1.3 Limitations of Capture-and-Examine Tests
2. Self-Checking Unit & Integration Tests
3. JUnit Testing
3.1 JUnit Basics
4. C++ Unit Testing
4.1 Google Test
4.2 Boost Test Framework

The Testing Process

Earlier we introduced this model of the basic testing process.

1. Automating the Oracle


Why Automate the Oracle?

If a bug is reported or a new feature requested, the first step is to add new tests that fail because of the bug or missing feature. Then we will know we have successfully fixed the problem when we are able to pass that test.

The best way to be sure programmers rerun the tests on a regular basis is to make the test run part of the regular build process (e.g., build the test runs into the project make file) and to make them self-checking.

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 a program produces output files, one can self-check by creating a file representing the expected correct output, then running the program to get the actual output file and using a simple comparison utility like the Unix diff or cmp commands to see if the actual output is identical to the expected output.

For system-level regression tests, this is even simpler. Once we have a program that passes our system tests, we run it on those tests and save the outputs. Those become the expected output files for later regression testing. (Remember, the point of regression testing is to determine if any behavior has changed due to recent updates to the code.)

If the program updates a database, it may be possible to capture entire databases in a similar fashion. Alternatively, we write database queries to check for changes in the records most likely to have been affected by a test.

On the other hand, if the program’s main function is to present information on a screen, self-checking is very difficult. Screen captures are often not much use, because we are unlikely to want to deal with changes where, say one window is a pixel wider or a pixel to the left of where it had been in a prior test. Self-checking tests for programs like this either require extremely fine control over all possible interactive inputs and graphics device characteristics, or they require a careful “design for testability” to record, in a testing log file, information about what is being rendered on the screen.[1]


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

In testing an ADT, we are not testing an individual function, but a collection of related functions. In some ways that makes thing easier, because we can use many of these functions to help test one another.


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));
}

Not only is this code simpler than writing your own search function as part of the test driver, it continues to work even if the data structure used to implement the ADT should be changed. What’s more, it is, in a sense, a more thorough test, since it tests two functions at once. Finally, there’s the simple fact that the test with the explicit loop probably won’t even compile, since it refers directly to data members that are almost certainly private.

In a sense, we have made a transition from white-box towards black-box testing. The new test case deliberately ignores the underlying structure.


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


  1. We’ll revisit this idea later in the semester when we discuss the MVC pattern for designing user interfaces.  ↩