Software Testing: Practical Discussion Continued

The code under test in the Practical raises several difficulties when it comes to coverage and testing. Part of this practical is concerned with showing what happens when code isn't designed to be tested, but perhaps these comments will get your creative juices flowing:

Private methods and fields

There are a couple of techniques you could use in order to test private methods if there's no way of adequately exercising them or verifying their operation through the public interface. One is to use reflection, and another is to use inner classes. Let's look at testing this simple (and useless) counter class:

Source for st.Counter:
package st;

public class Counter {

  private int m_count = 0;

  private void add(int amount) {
    m_count += amount;
  }

  public void tick() {
    add(1);
  }

}

The only public method is tick(), which uses the private method add() to increase the private count variable. So to see that anything is actually happening here we'll need to dig into its private members.

Reflection

Reflection takes more work but is cleaner since the tests remain separate from the software under test, and the software under test is unchanged. However we need to locate the private fields and methods, which takes a bit of work, and then modify them so that they're accessible to us (i.e. no longer private).

Source for st.CounterTest:
package st;

import static org.junit.Assert.*;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

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

public class CounterTest {

  Counter  m_counter;
  Class<?> m_cClass;
  Field    m_cCountField;
  Method   m_cAddMethod;

  @Before
  public void setUp() throws NoSuchFieldException, NoSuchMethodException {
    m_counter = new Counter();
    m_cClass = m_counter.getClass();
    // Find the private counter field:
    m_cCountField = m_cClass.getDeclaredField("m_count");
    // Modify the field so we can access it:
    m_cCountField.setAccessible(true);
    // To find the private add method, we have to identify it by name and
    // parameter signature:
    Class params[] = { int.class };
    m_cAddMethod = m_cClass.getDeclaredMethod("add", params);
    // Make the method accessible:
    m_cAddMethod.setAccessible(true);
  }

  @Test
  public void testConstructor() throws IllegalAccessException {
    int cCount = m_cCountField.getInt(m_counter);
    assertEquals("Count not initialised to 0!", cCount, 0);
  }

  @Test
  public void testTick() throws IllegalAccessException {
    m_counter.tick();
    int cCount = m_cCountField.getInt(m_counter);
    assertEquals("tick() didn't increase count to 1!", cCount, 1);
  }

  @Test
  public void testAdd() throws IllegalAccessException, InvocationTargetException {
    Object params[] = { -1 };
    m_cAddMethod.invoke(m_counter, params);
    int cCount = m_cCountField.getInt(m_counter);
    assertEquals("add(-1) didn't decrease count to -1!", cCount, -1);
  }

}

Inner classes

Inner classes are less work, but involve adding your test classes into the class under test. They'll be compiled as separate class files so could be removed from the release system, but their addition will have modified the class under test in order to create accessors for the inner class to get at the fields of the (outer) class under test.

Here you can see though that the actual test code is much simpler. Note that your inner test class must be static (otherwise it's expected to have a reference to a container instance, which JUnit doesn't know how to provide).

Source for st.Counter, with InnerCounterTest added:
package st;

import static org.junit.Assert.*;

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

public class Counter {

  private int m_count = 0;

  private void add(int amount) {
    m_count += amount;
  }

  public void tick() {
    add(1);
  }

  public static class InnerCounterTest {

    Counter m_counter;

    @Before
    public void setUp() {
      m_counter = new Counter();
    }

    @Test
    public void testConstructor() {
      assertEquals("Count not initialised to 0!",
          m_counter.m_count, 0);
    }

    @Test
    public void testTick() {
      m_counter.tick();
      assertEquals("tick() didn't increase count to 1!",
          m_counter.m_count, 1);
    }

    @Test
    public void testAdd() {
      m_counter.add(-1);
      assertEquals("add(-1) didn't decrease count to -1!",
          m_counter.m_count, -1);
    }

  }

}

Test suite

Here's a test suite to bind the above together:

Source for st.AllTests:
package st;

import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({
  st.CounterTest.class,
  st.Counter.InnerCounterTest.class
})

public class AllTests {
  // Everything's done by the annotations above.
}

main()

There's nothing to stop you from calling main() yourself. The only question is how you can test whether it's done what you expect.

Oracles

Some of the methods perform actions which you may find difficult to verify programmatically. If you really can't program your test suite to tell whether those actions have taken place, maybe your test suite could stop and ask somebody else to check those actions?

It's got to stop somewhere!

Finally, bear in mind that one of the critical decisions you always need to make is when to stop testing. This assignment is intended to take you a certain amount of time, and you should be careful to balance how you use that time. Look at the distribution of the marks, and make sure you apportion your effort appropriately!


Version 1.2, 2012/02/02 09:47:13


Home : Teaching : Courses : St : 2011-12 

Informatics Forum, 10 Crichton Street, Edinburgh, EH8 9AB, Scotland, UK
Tel: +44 131 651 5661, Fax: +44 131 651 1426, E-mail: school-office@inf.ed.ac.uk
Please contact our webadmin with any comments or corrections. Logging and Cookies
Unless explicitly stated otherwise, all material is copyright © The University of Edinburgh