Unit testing 单元测试没有返回值的类?

Unit testing 单元测试没有返回值的类?,unit-testing,tdd,mocking,Unit Testing,Tdd,Mocking,关于这个具体问题,我在教程中找不到太多 所以我有一个名为“Job”的类,它有公共的构造函数和一个公共的Run()函数。类中的所有内容都是私有的,并封装在类中。(你可能还记得这里的一篇老帖子,它的回复对我帮助很大) 这个Run()方法做了很多事情——将excel文件作为输入,从中提取数据,向第三方数据供应商发送请求,获取结果并将其放入数据库,并记录作业的开始/结束 此作业类在其运行方法中使用3个独立的接口/类(IConnection将连接到第三方供应商并发送请求,IParser将解析结果,IDat

关于这个具体问题,我在教程中找不到太多

所以我有一个名为“Job”的类,它有公共的构造函数和一个公共的Run()函数。类中的所有内容都是私有的,并封装在类中。(你可能还记得这里的一篇老帖子,它的回复对我帮助很大)

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

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

除了-我对如何测试Run()方法仍然有点迷茫-因为它是无效的,不会返回任何东西

在这种情况下,我是否应该向Run()方法添加一个返回值,该方法返回从Excel文件中提取的记录数?因为这是该函数中现在执行的唯一逻辑。。这不会在实际代码中处理,但会在单元测试中处理。。。这对我来说似乎有点难闻——但就真正的TDD而言,我是个新手

第二个问题——我是否应该创建一个名为IExcelExtractor的第四个类,它为我提供了这种逻辑?还是这有点像班级爆炸

即使我做了后者,如果Run()函数返回void,并且它的所有工作都是由模拟对象执行的,而模拟对象实际上什么都不做,我该如何测试它呢?我可以理解我的函数是否有一个有意义的返回值。。。但在这种情况下,我是一个很困惑的人


非常感谢您阅读了所有这些内容,如果您做到了这一点。

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


不要在run()方法中编写跟踪其执行的代码。如果您无法基于与协作者(mock)的交互来验证方法的操作,则表明需要重新考虑这些交互。这样做也会弄乱主线代码,增加维护成本。

当您注入模拟时,您会向运行类的构造函数传递一个测试类,您将询问该测试是否通过。例如,如果在构造函数中传递了excel文件,则可以测试IParser模拟是否获得了正确的请求。您可以通过自己的类来实现这一点,并在其中收集结果并测试它收集的内容,或者您也可以通过模拟框架来实现这一点,该框架为您提供了在不构建类的情况下表达此类测试的方法

我看到你用tdd标记了你的问题,但在真正的tdd中,你并没有真正得到这个问题(你得到了,但问得不同),因为你首先构建了测试,它定义了接口,而不是构建类接口,然后思考如何测试这个东西。测试驱动设计的需要。您仍然使用相同的技术(在本例中可能最终使用相同的设计),但问题会有点不同。

我问了一个问题

尽管(理论之上的感觉)我确实认为一些方法不需要单元测试,只要它们:

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

如果它们的功能(即调用顺序)至关重要,那么您必须验证是否满足内部功能。这意味着您必须验证(使用您的mock)这些方法是否使用了正确的参数和正确的顺序(如果有关系的话)调用。

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

为什么不创建一些已知值以从mock IConnection返回,只需通过mock IParser传递所有这些值,并将它们存储在mock IDataAccess中,然后在测试中检查mock IDataAccess中的结果是否与通过run()方法运行后mock 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);
    }
}
公共接口i连接{
公共列表findFoos();
}
公共接口IParser{
公共列表解析(列表原始oos);
}
公共接口IDataAccess{
公共作废保存(列表保存);
}
公共类作业实现可运行{
专用i连接;
专用IParser解析器;
私有数据访问;
公共作业(IConnection连接、IParser解析器、IDataAccess数据访问){
这个连接=连接;
this.parser=解析器;
this.dataAccess=dataAccess;
}
公开募捐{
List allFoos=connection.findFoos();
列出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)));
    }
}
public类MockConnection实现IConnection{
私人名单;
公共列表findFoos(){
返回foos;
}
公共void setFoos(列出foos){
this.foos=foos;
}
}
公共类MockParser实现IParser{
private int[]keepIndexes=new int[0];
公共列表解析(列表原始oos){
List parsedFoos=new ArrayList();
对于(int i=0;i// 'mockery' is the central framework object and Mock object factory
IParser mockParser   = mockery.NewMock<IParser>();

// Other dependencies omitted
Job     job          = new Job(mockParser);

// This just ensures this method is called so the return value doesn't matter
Expect.Once.On(mockParser).
    .Method("Parse").
    .WithAnyArguments().
    .Will(Return.Value(new object()));

job.Run();
mockery.VerifyAllExpectationsHaveBeenMet();