1

I am new to JUnit and looking for advice. To start myself off I picked a simple helper object, that is used to manage a Table of Strings. The tests have a progression that argues against the granularity encouraged in the guides I've read.

Trying to keep the granularity fine without duplicating tests, I have created some tests for related methods such AddAndSize or GetAndPut. However, having the code to setup the test in multiple tests seems odd, I'm really trying to get my feet on the ground with jUnit and find the balance of test granularity. Here is my Target and TestCase -

  • Have I started any bad habits that I should avoid?
  • Is this a "typical" use of jUnit?
  • When testing abstract classes, should I place the "Stub" target in the Tests or Main source folder?

DataTable is a Class to be tested

public class DataTable {
    private ArrayList<String> columnNames = new ArrayList<String>();
    private ArrayList<ArrayList<String>> theData = new ArrayList<ArrayList<String>>();

    public DataTable() {
    }

    public int size() {
        return theData.size();
    }

    public int cols() {
        return columnNames.size();
    }

    public void addCol(String name) {
        this.columnNames.add(name);
    }

    public int getCol(String name) {
        return columnNames.indexOf(name);
    }

    public String getCol(int index) {
        if (index < 0 | index >= columnNames.size()) {return "";}
        return columnNames.get(index);
    }

    public String getValue(int row, String name) {
        return getValue(row,this.getCol(name)); 
    }

    public String getValue(int row, int col) {
        if (row < 0 | row >= theData.size()) {return "";}
        if (col < 0 | col >= theData.get(row).size()) {return "";}
        return theData.get(row).get(col);
    }

    public ArrayList<String> getNewRow() {
        ArrayList<String> newRow = new ArrayList<String>();
        this.theData.add(newRow);
        return newRow;
    }
}

And here is the Test Case I've written.

public class DataTableTest {

    /**
     * Test Constructor
     */
    @Test
    public void testDataTableConstruction() {
        DataTable table = new DataTable();
        assertNotNull(table);
    }

    /**
     * Test GetNewRow and Size
     */
    @Test
    public void testGetNewRowAndSize() {
        DataTable table = new DataTable();
        assertEquals(0, table.size());
        ArrayList<String> row = table.getNewRow();
        assertNotNull(row);
        assertEquals(1, table.size());
    }

    /**
     * 
     */
    @Test
    public void testColsAndAddCol() {
        DataTable table = new DataTable();
        assertEquals(0, table.cols());
        table.addCol("One");
        table.addCol("Two");
        table.addCol("Three");
        assertEquals(3, table.cols());
    }

    /**
     * 
     */
    @Test
    public void testGetColInt() {
        DataTable table = new DataTable();
        table.addCol("One");
        table.addCol("Two");
        table.addCol("Three");
        assertEquals("One", table.getCol(0));
        assertEquals("Two", table.getCol(1));
        assertEquals("Three", table.getCol(2));
    }

    /**
     * 
     */
    @Test
    public void testGetColString() {
        DataTable table = new DataTable();
        table.addCol("One");
        table.addCol("Two");
        table.addCol("Three");
        assertEquals(0, table.getCol("One"));
        assertEquals(1, table.getCol("Two"));
        assertEquals(2, table.getCol("Three"));
        assertEquals(-1, table.getCol("Four"));
    }

    /**
     * 
     */
    @Test
    public void testGetValueIntString() {
        DataTable table = new DataTable();
        table.addCol("One");
        table.addCol("Two");
        table.addCol("Three");
        ArrayList<String> row = table.getNewRow();
        row.add("R1C1");
        row.add("R1C2");
        row.add("R1C3");
        row = table.getNewRow();
        row.add("R2C1");
        row.add("R2C2");
        row.add("R2C3");

        assertEquals("R1C1", table.getValue(0, "One"));
        assertEquals("R1C3", table.getValue(0, "Three"));
        assertEquals("R2C2", table.getValue(1, "Two"));
        assertEquals("", table.getValue(2, "One"));
        assertEquals("", table.getValue(0, "Four"));
    }

    /**
     * 
     */
    @Test
    public void testGetValueIntInt() {
        DataTable table = new DataTable();
        table.addCol("One");
        table.addCol("Two");
        table.addCol("Three");
        ArrayList<String> row = table.getNewRow();
        row.add("R1C1");
        row.add("R1C2");
        row.add("R1C3");
        row = table.getNewRow();
        row.add("R2C1");
        row.add("R2C2");
        row.add("R2C3");

        assertEquals("R1C1", table.getValue(0, 0));
        assertEquals("R1C3", table.getValue(0, 2));
        assertEquals("R2C2", table.getValue(1, 1));
        assertEquals("", table.getValue(2, 0));
        assertEquals("", table.getValue(0, 3));
    }
}
Mike Storey
  • 1,029
  • 3
  • 11
  • 21
  • I'm not quite sure what you're asking here. You don't want to create test data, yet you've got overlapping test conditions (Empty/Add/Get). It's also tough to determine what you're testing about the container object; are you using an ORM like Hibernate? Are you testing any complicated, non-vanilla database accesses (like advanced joins)? What is the end goal of a test for something like this? – Makoto May 23 '15 at 16:04
  • I'm sorry if my example confused you, I am wanting to test all methods of the class which is a simple container for a List of column names, and a List> of data. I was thinking to @test "TestEmptyColumns", TestAddGetColumn and TestColumnBounds methods, followed by similar test for the Row methods and finally for the get/put data methods. If I create each of these as individual test methods that have to stand alone, the test get/put data methods will have to duplicate the code from the add/getColumn and add/getRow tests. I don't like duplicate code.... – Mike Storey May 23 '15 at 16:16
  • Duplicate code is not nearly as bad in tests as it is in production code (tests should be [DAMP not DRY](http://stackoverflow.com/q/6453235/1079354)). The main issue I'm having is getting my head around what the object even *is*. Do you have the structure laid out for it? You also haven't answered the question if you're using an ORM layer or not. – Makoto May 23 '15 at 16:20
  • This is a simple "DataTable" that has only two attributes: ArrayList columns ArrayList> tableData With methods like "GetNewRow" and "Get(row,col) and Put(row,col). This is a simple transitive store used to "normalize" tabular data structures from a variety of sources (SQL, CSV, XML...). So there is no persistence to the object. – Mike Storey May 23 '15 at 16:43
  • I'm just starting out with jUnit, so I've started with a very simple helper class that I use to abstract "Table" processing. There is the Class that is being tested: – Mike Storey May 23 '15 at 19:12
  • Thank you for your questions, I have posted a more clearly stated question. – Mike Storey May 23 '15 at 19:51

3 Answers3

0

I suggest using the Enclosed runner, and having different static nested classes for each initial state you want to use for your tests:

@RunWith(Enclosed.class)
public class DataTableTest {

  @RunWith(JUnit4.class)
  public static class WhenTableIsEmpty {
    private final DataTable table = new DataTable();

    @Test
    public void rowCountShouldReturnZero() {
      assertEquals(0, table.rowCount());
    }

    @Test
    public void addingRowsShouldSucceed() {
      table.addRow("row1");
      assertEquals(1, table.rowCount());
    }

    ...
  }

  @RunWith(JUnit4.class)
  public static class WhenTableHasOneRow {
    private final DataTable table = new DataTable();

    @Before
    public void addOneRow() {
      table.addRow("row1");
    }

    @Test
    public void rowCountShouldReturnOne() {
      assertEquals(1, table.rowCount());
    }

    ...
  }
}

Note that the nested classes need to be static (I neglected to add the static keyword in my initial version of this answer)

When you run the tests in your IDE the test cases will have readable names like this:

  • DataTableTest.WhenTableIsEmpty.rowCountShouldReturnZero
  • DataTableTest.WhenTableIsEmpty.addingRowsShouldSucceed

Note you don't need to use the JUnit4 runner for the nested classes. You could also use Parameterized or Theories.

NamshubWriter
  • 23,549
  • 2
  • 41
  • 59
  • I was going to ask about the order of steps, but I see how it is working now, the @Before set's up the test for each step. Thank you very much. – Mike Storey May 23 '15 at 20:08
  • Newbee is stumped... JUnit4.class where is that class located? – Mike Storey May 23 '15 at 20:40
  • JUnit4 is here: http://junit.org/javadoc/latest/org/junit/runners/JUnit4html I recommend using an IDE like Eclipse or IntelliJ for development; they make it easy to find classes and fix imports. – NamshubWriter May 23 '15 at 21:55
0

There are only a handful of things that I can identify as worth testing:

  • getCol(int)
  • getValue(int, String)
  • getValue(int, int)
  • potentially getNewRow()

The reason I say this: you're relying on ArrayList for most of your functionality, and you should not test anything that exclusively delegates to a known and tested class.

The main things you want to do here when testing are:

  • Establish the state of the thing you're testing
  • Test only that thing

Let's walk through a test of getCol(int). I'll repost the code here for clarity.

public String getCol(int index) {
    if (index < 0 | index >= columnNames.size()) {return "";}
    return columnNames.get(index);
}

There are four things you can test:

  • index < 0 && index >= columnNames.size()
  • index >= 0 && index >= columnNames.size()
  • index < 0 && index < columnNames.size()
  • index >= 0 && index < columnNames.size()

Alternatively, if you change | to || you'd only have to test three things:

  • index < 0
  • index >= columnNames.size()
  • index >= 0 && index < columnNames.size()

The reason is that | isn't short-circuiting and both sides of that conditional will be evaluated.

Setting up your state will have to be on a test-by-test basis. This way, it's clear what you're testing, why it's failing, and it makes it tons easier to resolve. I won't spell out every test you need (since there are quite a few), but they read something like this:

@Test
public void testGetColWithIndexLessThanZero() {}

Be certain to fill in the state of your test with each one. If you notice yourself duplicating state generation, then and only then can you create a helper method in the test itself to help with that.

When testing abstract classes, should I place the "Stub" target in the Tests or Main source folder?

You can't instantiate an abstract class directly, so the only way you'd be able to test it is if you created a class that extended it. You could do this either directly (an actual class that extends the abstract class), or in the test (create an anonymous class that extends the abstract class).

Makoto
  • 104,088
  • 27
  • 192
  • 230
  • *"you should not test anything that exclusively delegates to a known and tested class"* I would take care in following this advice. While `ArrayList` is well tested, your class' use of `ArrayList` is not. – NamshubWriter May 23 '15 at 22:01
-1

I would just do it like any software, copy the common parts to one place and so forth. My suggestion:

public class MyTests {
  private DataTable table;

  @Before
  public void setup() {
    table = new DataTable();
    assertEquals(0, table.size());
    assertEquals(0, table.cols());
    table.addCol("One");
    table.addCol("Two");
    table.addCol("Three");
    assertEquals(3, table.cols());
  }

  /**
   * Test GetNewRow and Size
   */
  @Test
  public void testGetNewRowAndSize() {
    ArrayList<String> row = table.getNewRow();
    assertNotNull(row);
    assertEquals(1, table.size());
  }

  @Test
  public void testGetColInt() {
    assertEquals("One", table.getCol(0));
    assertEquals("Two", table.getCol(1));
    assertEquals("Three", table.getCol(2));
  }

  @Test
  public void testGetColString() {
    assertEquals(0, table.getCol("One"));
    assertEquals(1, table.getCol("Two"));
    assertEquals(2, table.getCol("Three"));
    assertEquals(-1, table.getCol("Four"));
  }

  private void addRows() {
    ArrayList<String> row = table.getNewRow();
    row.add("R1C1");
    row.add("R1C2");
    row.add("R1C3");
    row = table.getNewRow();
    row.add("R2C1");
    row.add("R2C2");
    row.add("R2C3");
  }

  @Test
  public void testGetValueIntString() {
    addRows();
    assertEquals("R1C1", table.getValue(0, "One"));
    assertEquals("R1C3", table.getValue(0, "Three"));
    assertEquals("R2C2", table.getValue(1, "Two"));
    assertEquals("", table.getValue(2, "One"));
    assertEquals("", table.getValue(0, "Four"));
  }

  @Test
  public void testGetValueIntInt() {
    addRows();
    assertEquals("R1C1", table.getValue(0, 0));
    assertEquals("R1C3", table.getValue(0, 2));
    assertEquals("R2C2", table.getValue(1, 1));
    assertEquals("", table.getValue(2, 0));
    assertEquals("", table.getValue(0, 3));
  }
}

Not sure if the asserts in the before are of any use, any tests will fail anyway if those don't hold. Anyway, no special magic in writing tests as I see..

kg_sYy
  • 1,127
  • 9
  • 26