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

How to unit test C# events with xUnit

我想对由依赖项引发的事件是否被正在测试的 class 订阅进行单元测试。 要设置上下文,我有以下接口和 classes.

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 class 的 Start() 方法进行单元测试。 我想知道我该怎么做:

单元测试的目的可以是:

  1. 在您想要的输出中验证逻辑结果
  2. 验证是否进行了关键调用(只有当我想确保其他开发人员不会错误地删除一段代码时我才会这样做,但通常会验证 是否进行某些调用是不必要的,甚至更糟,使得 不必要的可维护性工作)

话虽如此,您不需要测试语言的内部结构。例如,在这种情况下,您无需验证在注册事件时是否会调用注册的方法。语言的工作就是做到这一点。那是通过语言测试的。

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

注意:单元测试更多的是 activity 使代码可测试,而不是 activity 弄清楚如何编写测试。如果编写测试很困难,则表明代码不易测试。

        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 创建一个测试 class 来测试 OnSomethingHappened 做了什么,模拟它的依赖关系并验证给定的 soAndSoMapper mock returns 一些值,secondDependency 被调用为该值.尽管再一次,OnSomethingHappened 并没有做太多事情。因此,是否要测试这个是有争议的。

根据 this answer, this documentation 和@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);
    }
}