Java解耦数据库操作代码并使其可测试性



我想为下面的方法写一个测试

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

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

DatabaseHelper (ormlite)没有使用init(Context context)(这通常是我的android应用程序启动时的一些,但在我的测试中没有完成)时,它的getInstance()方法将返回null,并且从上面执行的方法将崩溃。

我该怎么做?我可以从我的测试中调用init(Context context),或者在调用它之前检查DatabaseManager.getInstance()是否为空。但这似乎更像是一种变通办法。在我看来,我不应该在这个方法中做任何数据库的东西,并尽可能地将ItemManager与数据库分开。

从一般设计的角度来看,理想的解决方案不是具体实现的形式,有什么想法吗?

我是单元测试的新手,很难将彼此之间的东西解耦。

在我看来,你的类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

你的单元测试应该集中在测试一个方法,而不是它使用的类的行为。

在我的例子中,我通过构造函数在ItemManager中注入DatabaseHelper,但你可以使用任何方法:构造函数,setter,依赖注入框架等

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

class ItemManager {
 ...
 // decoupling
 private DatabaseHelper instance = DatabaseHelper.getInstance();
 public void addItem(Item item) {
    items.add(0, item);
    instance.writeOneItem(item);
 }
}

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

你可以创建自己的类来包装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
    }
}

那么你可以在测试中注入FakeDBLayer,在生产中注入OrmLiteDBLayer

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

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

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

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

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

在你的常规代码中,你会使用
ClassToTest myClass = new ClassToTest();
myClass.setDatabaseHelper(DatabaseHelper.getInstance());

在你的测试中你会使用

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

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

正如其他人已经提到的,有一些模拟框架可以为您节省编写mockobject的一些工作。另外,我建议您检查一下依赖注入。

最新更新