如何对一个方法进行单元测试,其副作用是调用其他方法?

时间:2015-03-03 20:04:02

标签: unit-testing testing mocking tdd

以下是我的例子:

void doneWithCurrentState(State state) {
    switch (state) {
        case State.Normal:
            // this method is never actually called with State.Normal
            break;
        case State.Editing:
            controller.updateViewState(State.Normal);
            database.updateObjectWithDetails(controller.getObjectDetailsFromViews())
            break;
        case State.Focus:
            controller.updateViewState(State.Editing);
            break;
    }
}

我的controller在按下特定按钮时调用doneWithCurrentStatestates在屏幕上的位置与controller的观看次数不同。

如果当前状态为Normal,则该按钮将被隐藏。

如果按下当前状态为Editing的按钮,则会调用doneWithCurrentState方法(我说方法,因为它实际上在类``中),它应该改变控制器的视图状态为Normal并使用Object更新database中的ObjectDetails(这只是一个包含将用于更新Object的数据的结构)应该从控制器的视图中检索(即文本字段,复选框等)。

如果按下当前状态为Focus的按钮,它应该只返回Editing状态。

我正在对它进行单元测试:

void testDoneWithCurrentStateEditing() {
    mockController.objectDetails = ...;

    myClass.doneWithCurrentState(State.Editing);

    AssertEqual(mockController.viewState, State.Normal, "controller state should change to Normal");

    AssertTrue(mockDatabase.updateObjectWithDetailsWasCalled, "updateObjectWithDetails should be called");
    AssertEqual(mockDatabase.updatedWithObjectDetail, mockController.objectDetails, "database should be updated with corresponding objectDetails");
}

void testDoneWithCurrentStateFocus() {
    myClass.doneWithCurrentState(State.Focus);

    AssertEqual(mockController.viewState, State.Editing, "controller state should change to Editing");

    AssertFalse(mockDatabase.updateObjectWithDetailsWasCalled, "updateObjectWithDetails should not be called");
}

但似乎错了,似乎我断言方法调用已经发出然后我正在进行调用......这就像声明setter和getter方法一样。

测试doneWithCurrentState方法的正确方法是什么? 作为答案的一部分,我确实接受了“首先你应该重构方法以更好地区分这些问题......”。

谢谢。

3 个答案:

答案 0 :(得分:1)

如果你写的不是先测试,一个明显的写法就是写一个案例,然后复制粘贴到下一个案例中。在这种情况下,一个容易犯的错误就是忘记将参数更新为updateViewState()。所以(例如)你可能会发现自己从State.Focus转到State.Normal。您编写的测试虽然对您来说可能看起来很弱,但可以防止出现这种性质的错误。所以我认为它正在做它应该做的事情。

答案 1 :(得分:0)

首先,请考虑使用state machine进行状态转换,您将退出switch语句分支业务,这将大大简化您的测试。

接下来,将您的测试视为代码和设计气味的潜在来源。如果很难为一段代码编写测试 - 可能代码缺乏质量(破坏SRP,太耦合等)并且可以简化/改进。

void doneWithCurrentState(State state) {
     State nextState = this.stateMachine.GetNextState(state);
     controller.updateViewState(nextState);

     if(nextState == State.Editing) 
         database.updateObjectWithDetails(controller.getObjectDetailsFromViews());
}

然后你可以注意到你可以拉出对方法状态机的调用并传入nextState。

//whoever calls this method should get nextState from state machine.
void doneWithCurrentState(State nextState) {
     controller.updateViewState(nextState);

     if(nextState == State.Editing) 
         database.updateObjectWithDetails(controller.getObjectDetailsFromViews());
}

等等..你将在状态机测试中为状态转换编写简单的测试..你的整体代码复杂性下降,一切都很好!?好吧,你可以达到的善良水平几乎没有限制,我可以看到更多的方法可以进一步清理代码。

根据您的原始问题,如何测试您的类的代码调用“数据库”或“控制器”,并使用具有特定状态的适当参数进行调用。您正在“正确”执行此操作,即模拟是什么意思是做。但是,有更好的方法。考虑基于事件的设计。如果您的控制器可以触发像“NextState”这样的事件并且您的“数据库”对象可以只订阅它,该怎么办?然后你需要测试的所有测试需要的是触发正确的事件而不包括任何关于数据库的东西(消除依赖性:))

答案 2 :(得分:0)

我认为保罗的观点是:将基于传入状态的状态更改放入状态机,即一个可重复性的对象,以确定接下来会发生什么。这可能听起来很愚蠢,因为你将相同的代码移动到另一个对象,但至少这会让控制器节食。它不应该担心太多的细节本身是可维护的。

我担心updateViewState。为什么它采用与控制器用户交互回调相同的参数?你可以用不同的方式建模吗在没有查看信息流的情况下很难告诉你任何具体信息(带有注释的详细序列图可能会有所帮助),因为通常对这些问题的真正洞察在于调用堆栈中更深层次。如果不了解所有这些的含义,很难找到适合的固定解决方案。

可能有帮助的问题:

  • 如果State代表3个(?)用户交互,这些交互都通过同一个隧道,您可以对要采取的策略或命令进行建模吗?
  • 如果doneWithCurrentState表示完成了多种互动模式之一,您真的需要使用共享的doneWithCurrentState方法吗?难道你不能使用三种不同的回调吗?也许这是一种错误的抽象。 (“不要重复自己”isn't about code but about things that change (in)dependently