单元测试一个没有返回值的类?

时间:2009-10-22 13:47:54

标签: unit-testing tdd mocking

我在这个具体问题的教程中找不到多少......

所以我有一个名为'Job'的类,它有公共ctors和一个公共Run()函数。类中的所有内容都是私有的,并封装在类中。 (您可能还记得此Testing only the public method on a mid sized class?上的帖子较旧,回复对我有很大帮助)

这个Run()方法做了很多事情 - 将excel文件作为输入,从中提取数据,向第三方数据供应商发送请求,获取结果并将其放入数据库并记录开始/工作结束。

此Job类在其run方法中使用3个单独的接口/类,(IConnection将连接到第三方供应商并发送请求,IParser将解析结果,IDataAccess将结果保存到数据库)。所以现在,我的Run()方法中唯一真正的逻辑是提取excel输入并将其发送到其他类的链中。我创建了3个模拟类并在Job类ctor上使用DI,一切都很好,花花公子......

除了 - 我仍然有点迷失测试我的Run()方法 - 因为它是无效的并且不返回任何东西......

在这种情况下,我应该向Run()方法添加一个返回值,该方法返回从Excel文件中提取的记录数量吗?因为这是现在该函数中唯一完成的逻辑..这不会在实际代码中处理,但会在单元测试中...这对我来说似乎有点臭 - 但我是一个新的真正的TDD ......

第二个问题 - 我应该创建一个名为IExcelExtractor的第四个类,它为我做了那个逻辑吗?或者这是一类爆炸?

即使我做了后者,如果它返回void我将如何测试我的Run()函数,并且它的所有工作都是由真正无效的模拟对象执行的?我能理解我的函数是否有一个有意义的返回值...但在这种情况下我很困惑。

非常感谢你阅读所有这些,如果你做到这一点。

7 个答案:

答案 0 :(得分:17)

您所描述的内容is often called behavior verification(与状态验证相对)。它有它的支持者和批评者,但是对于几类课程来说,如果你想进行单元测试,它是城里唯一的游戏。

要对其行为仅限于与协作者交互的类进行单元测试,您通常会传递模拟协作者对象,这些对象的检测方式允许您以预期的方式验证其方法。

如果您要对您在问题中提到的类进行手工操作(哎呀!),您可以创建一个实现MockParser的{​​{1}}类,并添加记录其是否及如何方法被称为。

最好使用模拟框架来动态创建模拟,指定它们的预期,并验证这些期望。

这些天我一直在使用NMock2,测试看起来像这样:

IParser

答案 1 :(得分:5)

当你注入一个mock时,你会向Run类的构造函数传递一个测试类,如果测试通过,你会询问它。例如,根据您在构造函数中传递的excel文件,您可以测试IParser mock是否获得了正确的请求。你可以通过自己的类来完成这个任务,并在其中收集结果并测试它收集的内容,或者你可以通过一个模拟框架来实现,它可以让你在不构建类的情况下表达这样的测试。

我看到你用tdd标记了你的问题,但是在真正的tdd中你没有真正得到这个问题(你这样做,但提出了不同的要求),因为你首先构建了测试,它定义了接口,而不是构建类界面,然后思考你将如何测试这件事。需要测试驱动设计。您仍然使用相同的技术(在这种情况下可能最终使用相同的设计),但问题会有所不同。

答案 2 :(得分:3)

您提到您在自包含的类中使用了3个类/接口的模拟实现...

为什么不创建一些已知的值从模拟IConnection返回,只需通过模拟IParser传递它们,并将它们存储在模拟IDataAccess中 - 然后在测试检查中看到模拟IDataAccess中的结果与运行run()方法后来自模拟IConnection的输入的预期结果?

编辑添加示例 -

应用程序接口/类:

public interface IConnection {
    public List<Foo> findFoos();
}

public interface IParser {
    public List<Foo> parse(List<Foo> originalFoos);
}

public interface IDataAccess {
    public void save(List<Foo> toSave);
}

public class Job implements Runnable {
    private IConnection connection;
    private IParser parser;
    private IDataAccess dataAccess;

    public Job(IConnection connection, IParser parser, IDataAccess dataAccess) {
        this.connection = connection;
        this.parser = parser;
        this.dataAccess = dataAccess;
    }

    public void run() {
        List<Foo> allFoos = connection.findFoos();
        List<Foo> someFoos = parser.parse(allFoos);
        dataAccess.save(someFoos);
    }
}

模拟/测试类:

public class MockConnection implements IConnection {
    private List<Foo> foos;

    public List<Foo> findFoos() {
        return foos;
    }

    public void setFoos(List<Foo> foos) {
        this.foos = foos;
    }
}

public class MockParser implements IParser {

    private int[] keepIndexes = new int[0];

    public List<Foo> parse(List<Foo> originalFoos) {
        List<Foo> parsedFoos = new ArrayList<Foo>();
        for (int i = 0; i < originalFoos.size(); i++) {
            for (int j = 0; j < keepIndexes.length; j++) {
                if (i == keepIndexes[j]) {
                    parsedFoos.add(originalFoos.get(i));
                }
            }
        }
        return parsedFoos;
    }

    public void setKeepIndexes(int[] keepIndexes) {
        this.keepIndexes = keepIndexes;
    }
}

public class MockDataAccess implements IDataAccess {
    private List<Foo> saved;

    public void save(List<Foo> toSave) {
        saved = toSave;
    }

    public List<Foo> getSaved() {
        return saved;
    }
}

public class JobTestCase extends TestCase {

    public void testJob() {
        List<Foo> foos = new ArrayList<Foo>();
        foos.add(new Foo(0));
        foos.add(new Foo(1));
        foos.add(new Foo(2));
        MockConnection connection = new MockConnection();
        connection.setFoos(foos);
        int[] keepIndexes = new int[] {1, 2};
        MockParser parser = new MockParser();
        parser.setKeepIndexes(keepIndexes);
        MockDataAccess dataAccess = new MockDataAccess();
        Job job = new Job(connection, parser, dataAccess);
        job.run();
        List<Foo> savedFoos = dataAccess.getSaved();
        assertTrue(savedFoos.length == 2);
        assertTrue(savedFoos.contains(foos.get(1)));
        assertTrue(savedFoos.contains(foos.get(2)));
        assertFalse(savedFoos.contains(foos.get(0)));
    }
}

答案 3 :(得分:2)

TDD的想法基本上是通过坚持它,你将编写易于测试的代码,因为你首先针对缺少实现的接口编写测试,然后编写代码来进行测试通过。您似乎在测试之前编写了Job类。

我发现您可以更改Job.Run实现,在这种情况下,如果您希望代码可以测试,您应该对其执行某些操作以便能够读取您需要测试的值。

答案 4 :(得分:1)

如果你的run()方法所做的唯一事情是调用其他对象,那么你测试它,但验证是否调用了模拟。具体如何做到这一点取决于模拟包,但通常你会找到某种“期望”的方法。

不要在run()方法中编写将跟踪其执行的代码。如果您无法根据与协作者(模拟器)的交互来验证方法的操作,则表明需要重新考虑这些交互。这样做也会使主线代码混乱,从而增加了维护成本。

答案 5 :(得分:1)

我问过similar question

虽然(对理论的感觉)我确实认为某些方法不需要单元测试,只要(和直到)它们:

  • 不返回任何值
  • 不要更改可以检查的类或系统的内部状态
  • 不依赖于其他任何东西(作为输入或输出)而不是模拟

如果他们的功能(即呼叫顺序)至关重要,则必须验证是否满足内部功能。这意味着您必须验证(使用您的模拟)已使用正确的参数和正确的顺序调用这些方法(如果重要)。

答案 6 :(得分:1)

首先,由于你的run()方法是一种工作流程启动器,并且你需要在工作流程中执行几个步骤,我认为你需要多个单元测试,甚至可能需要拆分现有的分为几个较小的,分别对应于工作流程中的一个步骤。

通过这种方式,您还可以单独测试工作流程的每个步骤,如果工作流程失败的任何时候,这些较小的单元测试将允许您更容易识别故障部分(失败的步骤)

但也许情况已经如此,我不知道你是否已经有过这种分裂。

无论如何,回到你的run()方法,答案就在于你的问题:

  

这个Run()方法做了很多事情 - 将excel文件作为输入,从中提取数据,向第三方数据供应商发送请求,获取结果并将其放入数据库并记录开头/工作结束

所以你有:

  • 一些输入数据(来自excel文件)

  • 一些“输出”数据或者更确切地说是wokflow的结果。

为了使run()成功,您需要检查:

a)请求已发送给第三方和/或已收到结果。我不知道哪一个更容易检查,但至少你可以记录请求/响应并检查正在执行的操作的日志(在单元测试中)。这将确保整个工作流程的执行(我们可以想象在工作流结束时数据库中存在正确数据的情况,但不是因为运行正常,而是因为数据已经存在或者那些行 - 如果在测试之前清除,则不会删除某些数据)

b)检查数据库中是否存在因工作流程而在适当位置插入/更新的正确值(相对于输入值)。

c)你甚至可以检查你提到的日志(作业的开始/结束),看看两个操作之间延迟的有效性(如果你知道它不能比10秒更快地工作,如果你的日志说工作在1秒内完成,你会知道出了什么问题......)


编辑:作为上述a)之前的第一个测试,您可能还需要检查输入数据,因为您可能会想到错误(缺少excel文件或内容已更改,因此您'输入错误等等)