如何编写具有渐进状态的 JUnit 测试

How to write JUnit tests with progressive state

我是 JUnit 的新手,正在寻求建议。首先,我选择了一个简单的辅助对象,用于管理 Table 个字符串。这些测试的进展与我读过的指南中鼓励的粒度相悖。

为了在不重复测试的情况下保持良好的粒度,我为相关方法(如 AddAndSize 或 GetAndPut)创建了一些测试。然而,让代码在多个测试中设置测试似乎很奇怪,我真的想用 jUnit 站稳脚跟并找到测试粒度的平衡。这是我的目标和测试用例 -

数据Table是一个Class待测

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

这是我写的测试用例。

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

我建议使用 the Enclosed runner,并为每个要用于测试的初始状态设置不同的静态嵌套 类:

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

    ...
  }
}

请注意,嵌套的 类 需要是 static(我忽略了在此答案的初始版本中添加 static 关键字)

当您 运行 中的测试 IDE 时,测试用例将具有如下可读的名称:

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

请注意,您不需要为嵌套的 类 使用 JUnit4 运行ner。您也可以使用 ParameterizedTheories.

我会像任何软件一样,将通用部分复制到一个地方等等。我的建议:

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

不确定前面的断言是否有用,如果这些断言不成立,任何测试都会失败。无论如何,在我看来,编写测试没有什么特别的魔法..

只有少数几项我认为值得测试:

  • getCol(int)
  • getValue(int, String)
  • getValue(int, int)
  • 可能 getNewRow()

我这样说的原因:你的大部分功能都依赖 ArrayList,你不应该测试任何专门委托给已知和测试的 class.

测试时你想在这里做的主要事情是:

  • 确定您正在测试的事物的状态
  • 只测试那个东西

让我们来看看 getCol(int) 的测试。为了清楚起见,我将在此处重新发布代码。

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

您可以测试四项内容:

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

或者,如果您将 | 更改为 ||,您只需测试三件事:

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

原因是 | 没有短路,条件的两边都会被计算。

设置您的状态必须在逐个测试的基础上进行。通过这种方式,您可以清楚地了解您正在测试的内容、失败的原因,并且可以更轻松地解决问题。我不会详细说明您需要的每项测试(因为有很多),但他们读的是这样的:

@Test
public void testGetColWithIndexLessThanZero() {}

确定每一项填写你的考试状态。如果您注意到自己复制了状态生成,那么 那么您可以在测试本身中创建一个辅助方法来帮助解决这个问题。

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

您不能直接实例化一个抽象 class,因此您能够测试它的唯一方法是创建一个扩展它的 class。您可以直接执行此操作(扩展抽象 class 的 actual class),或在测试中(创建扩展的匿名 class摘要 class).