【问题标题】:Approach to test public methods depending on private methods根据私有方法测试公共方法的方法
【发布时间】:2023-03-19 06:55:01
【问题描述】:

我正在尝试向遗留代码添加测试,当我开始添加代码时,我感觉有些地方出了问题。

在以下代码中,公共方法 RegisterChange 调用了两个私有方法:

  1. 获取要存储的对象
  2. 存储对象
public class ChangeService {

    IRepository repository;

    public ChangeService(IRepository repository){
        this.repository = repository;
    }

    public bool RegisterChange( int entityId ){ 
        var entity = GetParsedEntity( entityId );       
        SaveEntity( entity );
        return true;
    }

    private Entity GetParsedEntity( int id ) {
        var entity = repository.GetEntityById( id );
        return new Entity{ Name = entity.Name };
    }

    private void SaveEntity( Entity entity ) {
        repository.Save( Entity );
    }
}

public class ChangeServiceFact(){

    [Fact]
    public void When_valid_entity__Should_save_entity(){

        var mock = new Mock<IRepository>();
        var service = new ChangeService(mock.object);

        var result = service.RegisterChange( 0 );

        Assert.True(result);
    }   
}

所以,当我模拟存储库时,我必须去检查私有方法的代码以了解要模拟哪些操作。

我在这种方法中看到的问题是,由于代码不仅测试了测试主体(公共方法)而且还测试了私有方法,所以通过查看测试结果并不清楚哪个应该是测试结果测试对象(公共方法)。

如果稍后有人决定修改一个私有方法(例如从 GetParsedEntity 抛出异常),测试将继续正确通过,但客户端代码可能会因为此更改而失败。

在这种特殊情况下,我使用 C#、XUnit 和 Moq,但我认为这是一个更一般的测试问题。

【问题讨论】:

  • 测试肯定不会通过,因为调用RegisterChange时会抛出异常。如果它有时会抛出异常,那么由做出更改的人来为此添加测试。它仍应符合公共方法的 documented (ahem) 行为。
  • @JonSkeet,让我们说对 GetParsedInt 的更改不是破坏性的,而是一项新功能,在这种情况下,旧测试并没有意识到这一点。我知道一个新的测试会选择新的功能测试。所以,我的问题是“这是正常的方法吗?” (只是给它一个名字)。谢谢。
  • 这样说:GetParsedEntity 的代码是否内联到公共方法中不应该影响测试。这是一个实现细节。所以,如果你没有破坏任何东西,只是添加一个单独测试的新功能,你关心什么?在我看来是合理的。
  • 关键是在设计测试时不要对实现感兴趣。您应该对文档/合同描述的效果感兴趣。将“它的目的是什么”与“它的目的是如何实现它”分开。 (这就是使用假货而不是模拟物也派上用场的地方......)
  • @andymaster01:当我们讨论模拟/假货主题时,您可能想阅读一下this question。长话短说,现在 mockfake 之间的区别相当模糊。

标签: c# java unit-testing tdd moq


【解决方案1】:

我在这种方法中看到的问题是,由于代码不仅测试了测试主体(公共方法)而且还测试了私有方法,所以通过查看测试结果并不清楚哪个应该是测试结果测试对象(公共方法)。

您提到的测试对象在不知道其完整合同的情况下没有明显的效果。这里的完整合同是什么?上面提到的公共方法构造函数,它需要依赖。这里重要的是依赖关系,应该测试与这种依赖关系的交互。私有方法(一如既往)是实现细节 - 与单元测试无关

话虽如此,让我们回到合同上来。测试对象的实际合同是什么(ChangeService方法)?要根据某个 id 从存储库中检索对象,请创建不同的对象并将后者保存在同一存储库中。这是你的测试。

[Fact]
public void ChangeService_StoresNewEntityInRepository_BasedOnProvidedId()
{
    const string ExpectedName = "some name";
    var otherEntity = new OtherEntity { Name = ExpectedName };
    var mock = new Mock<IRepository>();
    var service = new ChangeService(mock.object);
    mock.Setup(m => m.GetEntityById(0)).Return(otherEntity);

    service.RegisterChange(0);

    mock.Verify(m => m.SaveEntity(It.Is<Entity>(e => e.Name == ExpectedName));
} 

【讨论】:

  • 所以,没关系,即使查看RegisterChange 方法,您也看不到IRepository 的直接用法,但是查看测试是否有该类的模拟?我认为这是我主要关心的问题。谢谢。
  • @andymaster01:没关系。这就是这个特定对象的工作方式 - 它与依赖项交互,对外部世界没有可见的结果(返回值/状态更改)。要检查其合同是否已履行,您询问依赖项,而不是测试对象本身。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-06
  • 1970-01-01
  • 2011-09-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多