如何使用xUnit对C#事件进行单元测试

6
我希望能对依赖项引发的事件进行单元测试,以检测被测试类是否已经订阅了该事件。 为了设置上下文,以下是接口和类。

ITestedService.cs

public interface ITestedService
{
  Task Start();
  Task Stop();
}

IDependency.cs

public interface IDependency
{
    event EventHandler<SoAndSoEventArgs> SomethingHappened;
    Task Start();
    Task Stop();
}

ISecondDependency

public interface ISecondDependency
{
    Task DoYourJob(SoAndSo soAndSo);
}

TestedService.cs

public class TestedService : ITestedService
{
    readonly IDependency m_dependency;
    readonly ISecondDependency m_secondDependency;

    public TestedService(
        IDependency dependency,
        ISecondDependency secondDependency)
    {
        m_dependency = dependency;
        m_secondDependency = secondDependency;
    }

    public async Task Start()
    {
        m_dependency.SomethingHappened +=  OnSomethingHanppened;
        await m_dependency.Start();
    }

    private async void OnSomethingHanppened(object sender, SoAndSoEventArgs args)
    {
        SoAndSo soAndSo = SoAndSoMapper.MapToDTO(args);
        await m_secondDependency.DoYourJob(soAndSo),
    }

}

在上文的基础上,我想使用xUnitTestedService类的Start()方法进行单元测试。

  • 断言事件是否已连接到处理程序。
  • 模拟触发IDependency.SomethingHappened事件。
  • 验证OnSomethingHappened方法是否被执行。
  • 验证是否调用了ISecondDependency.DoYourJob(soAndSo)方法。

1
据我所知,您需要引发m_dependencyMock实例的SomethingHappened事件。也许这个链接有所帮助?它展示了如何使用m_dependencyMock.Raise - Zev Spitz
那么,我 m_dependencyMock.Raise( 是 act 的一部分,对吧?@ZevSpitz - Jins Peter
1
是的,我想是这样。 (免责声明:我没有Moq方面的经验,也没有模拟方面的经验。) - Zev Spitz
我宁愿将单元测试部分作为答案而不是问题。 - Jins Peter
2个回答

7

根据这个答案这篇文档以及@ZevSpitz在评论中提供的指导,我成功地为Start()编写了以下测试。

虽然我无法验证是否执行了相同的代码路径OnSomethingHappened,或者是否是其他订阅调用了m_secondDependencyMock.DoYourJob(soAndSo)

TestedServiceTest.cs

public class TestedServiceTest
{
    readonly Mock<IDependency> m_dependencyMock;
    readonly Mock<ISecondDependency> m_secondDependencyMock;

    ITestedService testedService;

    public TestedServiceTest()
    {
        m_dependencyMock = new Mock<IDependency>();
        m_secondDependencyMock = new Mock<ISecondDependency>();
        testedService = new TestedService(m_dependencyMock.Object, m_secondDependencyMock.Object);
    }

    [Fact]
    public async Start_DependencyStartInvoked()
    {
        // Arrange
        m_dependencyMock.Setup(x=> x.Start()).Verifyable();

        // Act 
        await testedService.Start();

        // Assert
        //This tests if the IDependecy.Start is invoked once.
        m_dependencyMock.Verify(x=>x.Start(), Times.Once);
    }

    [Fact]
    public async Start_EventListenerAttached()
    {
        // Arrange
        m_dependencyMock.Setup(x=> x.Start()).Verifyable();
        m_dependencyMock.SetupAdd(m => m.SomethingHappened += (sender, args) => { });

        // Act 
        await testedService.Start();

        // Assert
        // The below together with SetupAdd above asserts if the TestedService.Start adds a new eventlistener
        // for IDependency.SomethingHappened
        m_dependencyMock.VerifyAdd(
            m => m.SomethingHappened += It.IsAny<EventHandler<SoAndSoEventArgs>>(), 
            Times.Exactly(1));
    }

    [Fact]
    public async Start_SomthingHappenedInvoked_HandlerExecuted()
    {
        // Arrange
        m_dependencyMock.Setup(x=> x.Start()).Verifyable();
        m_secondDependencyMock.Setup(x=> x.DoYourJob(It.IsAny<SoAndSo>())).Verifyable();

        // Act
        await testedService.Start();
        // This will fire the event SomethingHappened from m_dependencyMock.
        m_dependencyMock.Raise(m => m.SomethingHappened += null, new SoAndSoEventArgs());

        // Assert
        // Assertion to check if the handler does its job.
        m_secondDependencyMock.Verify(x=> x.DoYourJob(It.IsAny<SoAndSo>()), Times.Once);
    }
}


2

单元测试的目的可以是:

  1. 验证逻辑结果是否符合预期输出
  2. 验证关键调用是否被执行(如果我想确保其他开发人员不会错误地删除代码片段,我才会这样做,但通常情况下验证某个调用是否被执行并不必要,甚至更糟糕,会增加不必要的可维护性工作)

话虽如此,您不需要测试语言的内部。例如,在这种情况下,您不需要验证注册事件时,已注册的方法是否会被调用。这是语言的工作。语言已经测试过了。

因此,您已经验证了Start方法是否执行了您预期的调用。顺便提一下,正如我上面提到的,只有在存在原因(例如上述第2个目的)时才有意义这样做。现在您知道OnSomethingHappened将被触发。语言保证了这一点。您想测试的是OnSomethingHappened中的实际实现。为此,您需要通过使其可达(访问修饰符private无法使用)并使其依赖项也可模拟(SoAndSoMapper无法模拟)来使此方法更易于测试。

注意:单元测试更多的是使代码可测试的活动,而不是弄清楚如何编写测试的活动。如果编写测试很困难,那可能意味着代码不易于进行测试。
        public class TestedService
    {
        readonly IDependency m_dependency;
        readonly ISomethingDoer m_somethingDoer;

        public TestedService(
            IDependency dependency,
            ISomethingDoer somethingDoer)
        {
            m_dependency = dependency;
            m_somethingDoer = somethingDoer;
        }

        public async Task Start()
        {
            m_dependency.SomethingHappened += m_somethingDoer.OnSomethingHanppened;
            await m_dependency.Start();
        }
    }

    interface ISomethingDoer
    {
       Task OnSomethingHanppened(object sender, SoAndSoEventArgs args);
    }
    class SomethingDoer : ISomethingDoer
    {
        readonly ISecondDependency m_secondDependency;
        readonly ISoAndSoMapper m_soAndSoMapper;
        public SomethingDoer(ISecondDependency secondDependency, ISoAndSoMapper soAndSoMapper)
        {
           m_secondDependency = secondDependency;
m_soAndSoMapper = soAndSoMapper;
        }

        public async Task OnSomethingHanppened(object sender, SoAndSoEventArgs args)
        {
            SoAndSo soAndSo = m_soAndSoMapper.MapToDTO(args);
            await m_secondDependency.DoYourJob(soAndSo),
        }
    }

现在,您可以通过为SomethingDoer创建一个测试类,模拟其依赖项并验证例如给定soAndSoMapper模拟返回某个值时,secondDependency是否使用该值调用。尽管再次强调,OnSomethingHappened并没有做太多事情。因此,您可以争论是否要测试它。

是的,2就是我的目的。我想确保代码不被删除。我不在这里测试框架或语言。我试图让我的下一个开发人员知道代码存在的目的,不要将其删除。 - Jins Peter
无论如何,投票是关于"OnSomethingHappened不做太多事情。因此,你是否想测试这个是有争议的"。 - Jins Peter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接