Java解耦数据库操作代码&让它可测试

时间:2014-08-05 09:28:04

标签: java unit-testing design-patterns

我想为以下方法编写测试

public void addItem(Item item) {

    items.add(0, item);
    DatabaseHelper.getInstance().writeOneItem(item);
}

该类称为ItemManager,它的职责是管理用户可以保存到列表或从列表中删除的项目。它应与保留列表中项目的Sqlite数据库保持同步。

DatabaseHelper(ormlite)没有init(Context context)时(通常在我的Andoid应用启动时播放,但在我的测试中未完成),它的{{1} }}方法将返回getInstance(),并且从上面执行的方法将崩溃。

我该怎么办?我可以从我的测试中调用null,或者在调用任何内容之前检查init(Context context)是否为null。但这似乎更像是一种解决方法。在我看来,我不应该在这个方法中做任何数据库的东西,并尝试尽可能地从数据库中分离ItemManager。

关于理想解决方案的外观的任何想法,不是从具体实施的形式,而是从一般的设计角度来看?

我是单位测试的新手,并且很难将相互之间的东西分离。

4 个答案:

答案 0 :(得分:5)

在我看来,您的班级ItemManager必须致电DatabaseHelper来编写该项目,但您的单元测试只是想确保它。您不想测试DatabaseHelper实际上是否在数据库中写入该项目,这将是另一项测试。

我会修改你的类的设计:DatabaseHelper.getInstance()不应该直接在方法中完成。您的ItemManager应该有一个包含DatabaseHelper实例的私有字段。这样你就可以模拟它并验证它是否被调用。

例如使用Mockito

public void addItem(Item item) {
    items.add(0, item);
    this.databaseHelper.writeOneItem(item);
}

@Test
public void my_test() {
    // GIVEN
    DatabaseHelper databaseHelper = mock(DatbaseHelper.class);
    ItemManager manager = new ItemManager(databaseHelper);
    Item item = new Item()

    // WHEN
    manager.addItem(item);

    // THEN
    verify(databaseHelper).writeOneItem(item); // This verifies that the method writeOneItem of the "mock" is called with the "item" parameter
}

// Another test would check that the item is added to the "items" collection

您的单元测试应该专注于测试ONE方法而不是它使用的类的行为。

在我的示例中,我将DatabaseHelper注入ItemManager via构造函数,但您可以使用任何方法:constructor,setter,dependency injection framework等。

答案 1 :(得分:3)

打破静态依赖并使用模拟框架(如mockito)

class ItemManager {
 ...
 // decoupling
 private DatabaseHelper instance = DatabaseHelper.getInstance();

 public void addItem(Item item) {
    items.add(0, item);
    instance.writeOneItem(item);
 }
}

使用mockito:

class ItemManagerTest{
 // declare mock service
 @Mock
 DatabaseHelper instance;

 // inject mock service into your about to be tested class
 @InjectMocks
 ItemManager manager;

 @Test
 public void test() {
  // Given
  Item item = new Item();
  ...

  // When
  manager.addItem(item);

  // Then
  // assert that the service has been called with the right parameters
  verify(instance).writeOneItem(item);
 }

答案 2 :(得分:1)

您可以创建自己的包装DatabaseHelper的类,假设您将其称为MyDBLayer

class abstract MyDBLayer {
    public void writeOneItem(Item item);
}

class OrmLiteDBLayer {
    public void writeOneItem(Item item) {
        DatabaseHelper.getInstance().writeOneItem(item);
    }
}

class FakeDBLayer {
    public void writeOneItem(Item item) {
        // do nothing
    }
}

您可以在测试和生产中的OrmLiteDBLayer中注入FakeDBLayer。

答案 3 :(得分:1)

我想你想测试方法的行为,即items实际上是否包含新项,而不是数据是否写入数据库。

我使用依赖注入和模拟对象。

使用像DatabseHelper.getInstance()这样的东西绝对方便,但很难测试。我将测试改为

public class ClassToTest {

  private DatabaseHelper databaseHelper;

  public void setDatabaseHelper(DatabaseHelper databaseHelper) {
    this.databaseHelper = databaseHelper;
  }

  public void addItem(Item item) {
    items.add(0, item);
    databaseHelper.writeOneItem(item);
  }
}

接下来,我将使用方法IDatabaseHelper引入接口void writeOneItem(Item item),并让DatabaseHelper实现此接口。另外,我还创建了一个MockDatabaseHelper,它也实现了接口。

在常规代码中,您可以使用

ClassToTest myClass = new ClassToTest();
myClass.setDatabaseHelper(DatabaseHelper.getInstance());

在您的测试中,您使用

ClassToTest myClass = new ClassToTest();
myClass.setDatabaseHelper(new MockDatabaseHelper());

MockDatabaseHelper中的实现可以清空或是一个简单的日志语句。如果您的类使用DatabaseHelper中的其他方法,您还需要添加这些接口,并向MockDatabaseHelper添加一个实现模拟真实DatabaseHelper行为的实现。

正如其他人已经提到的那样,有一些模拟框架可以为您节省编写MockObjects的一些工作。 另外,我建议一般查看Dependency Injection