单元测试的可重用设置

本文关键字:设置 单元测试 | 更新日期: 2023-09-27 18:37:03

我正在使用xUnit和Moq来编写我的单元测试,并且我的各种测试中有很多重复的代码,我想以某种可重用的方式提取出来。

重复代码

var note = new Note { Id = Guid.NewGuid() };
    var subVersion = new Mock<SubmissionVersion>();
    subVersion.Setup(x => x.Notes.Remove(note));
    var repo = new Mock<IRepository>();
    repo.Setup(x => x.GetById<Note>(note.Id)).Returns(note);
    repo.Setup(x => x.GetById<SubmissionVersion>(It.IsAny<Guid?>())).Returns(subVersion.Object);

给定以下测试,如何清理它们以免出现太多重复?

[Fact]
public void Should_CallRepoGetNoteByIdOnce()
{
    // Arrange
    var note = new Note { Id = Guid.NewGuid() };
    var subVersion = new Mock<SubmissionVersion>();
    subVersion.Setup(x => x.Notes.Remove(note));
    var repo = new Mock<IRepository>();
    repo.Setup(x => x.GetById<Note>(note.Id)).Returns(note);
    repo.Setup(x => x.GetById<SubmissionVersion>(It.IsAny<Guid?>())).Returns(subVersion.Object);
    // Act
    SubmissionVersion.DeleteNote(repo.Object, subVersion.Object, note.Id.Value);
    // Assert
    repo.Verify(x => x.GetById<Note>(note.Id), Times.Once());
}
[Fact]
public void Should_CallSubmissionVerionNotesRemoveOnce()
{
    // Arrange
    var note = new Note { Id = Guid.NewGuid() };
    var subVersion = new Mock<SubmissionVersion>();
    subVersion.Setup(x => x.Notes.Remove(note));
    var repo = new Mock<IRepository>();
    repo.Setup(x => x.GetById<Note>(note.Id)).Returns(note);
    repo.Setup(x => x.GetById<SubmissionVersion>(It.IsAny<Guid?>())).Returns(subVersion.Object);
    // Act
    SubmissionVersion.DeleteNote(repo.Object, subVersion.Object, note.Id.Value);
    // Assert
    subVersion.Verify(x => x.Notes.Remove(note), Times.Once());
}
[Fact]
public void Should_CallRepoSaveSubmissionVersionOnce()
{
    // Arrange
    var note = new Note { Id = Guid.NewGuid() };
    var subVersion = new Mock<SubmissionVersion>();
    subVersion.Setup(x => x.Notes.Remove(note));
    var repo = new Mock<IRepository>();
    repo.Setup(x => x.GetById<Note>(note.Id)).Returns(note);
    repo.Setup(x => x.GetById<SubmissionVersion>(It.IsAny<Guid?>())).Returns(subVersion.Object);
    // Act
    SubmissionVersion.DeleteNote(repo.Object, subVersion.Object, note.Id.Value);
    // Assert
    repo.Verify(x => x.Save(subVersion.Object), Times.Once());
}
[Fact]
public void Should_CallRepoDeleteNoteOnce()
{
    // Arrange
    var note = new Note { Id = Guid.NewGuid() };
    var subVersion = new Mock<SubmissionVersion>();
    subVersion.Setup(x => x.Notes.Remove(note));
    var repo = new Mock<IRepository>();
    repo.Setup(x => x.GetById<Note>(note.Id)).Returns(note);
    repo.Setup(x => x.GetById<SubmissionVersion>(It.IsAny<Guid?>())).Returns(subVersion.Object);
    // Act
    SubmissionVersion.DeleteNote(repo.Object, subVersion.Object, note.Id.Value);
    // Assert
    repo.Verify(x => x.Delete(note), Times.Once());
}
[Fact]
public void Should_CallRepoGetSubmissionVersionByIdOnce()
{
    // Arrange
    var note = new Note { Id = Guid.NewGuid() };
    var subVersion = new Mock<SubmissionVersion>();
    subVersion.Setup(x => x.Notes.Remove(note));
    var repo = new Mock<IRepository>();
    repo.Setup(x => x.GetById<Note>(note.Id)).Returns(note);
    repo.Setup(x => x.GetById<SubmissionVersion>(It.IsAny<Guid?>())).Returns(subVersion.Object);
    // Act
    SubmissionVersion.DeleteNote(repo.Object, subVersion.Object, note.Id.Value);
    // Assert
    repo.Verify(x => x.GetById<SubmissionVersion>(subVersion.Object.Id), Times.Once());
}
[Fact]
public void Should_RemoveNotesFromSubmissionVersion()
{
    // Arrange
    var repo = new CompositeRepository().GenerateCompositeRepository<Guid?>(typeof(SubmissionVersion), typeof(Note));
    var subVersion = new SubmissionVersion { Id = Guid.NewGuid() };
    var note = new Note { Id = Guid.NewGuid(), Content = "Test Note" };
    repo.Save(note);
    subVersion.Notes.Add(note);
    // Act
    subVersion.Notes.ToList().ForEach(x => SubmissionVersion.DeleteNote(repo, subVersion, x.Id.Value));
    // Assert
    Assert.Null(repo.GetById<Note>(note.Id));
}

有什么建议/模式是最佳实践吗?

单元测试的可重用设置

我通常通过创建一个单元测试上下文对象来解决这个问题,该对象将可重用的模拟公开为公共属性。该对象将在内部设置公共模拟,并将它们公开为公共属性。您可以在此类中定义许多可重用的模拟。

前任:

public class UnitTestContext
{
   public Mock<IRepository> Repo {get;set;}

   public UnitTestContext()
   {
      // create suitable note / subversion objects 
      // either by passing them in or new-ing them up directly with default values. 
      Repo = new Mock<IRepository>();
      Repo.Setup(x => x.GetById<Note>(note.Id)).Returns(note);
      Repo.Setup(x => x.GetById<SubmissionVersion>(It.IsAny<Guid?>())).Returns(subVersion.Object);
   }
}

然后,测试可以创建一个实例:

[Fact]
public void Some_Test_In_Need_Of_A_Mocked_Repository()
{
   var ctx = new UnitTestContext();
   SubmissionVersion.DeleteNote(ctx.Repo.Object, subVersion.Object, note.Id.Value);
}

我更喜欢这种方法,而不是将模拟定义为测试类中的成员,因为 UnitTestContext 可以在测试类之间重用。

如果在返回值时需要更大的灵活性,还可以在构造模拟时将对象传递给上下文。还可以通过 Repo 属性添加到类外部的模拟。

一般来说,xUnit的人不喜欢测试设置的想法。但是如果需要,可以将一些常用对象公开为测试类的私有成员,并使用无参数 ctor 进行初始化。阅读与其他测试框架的属性比较。

private readonly Mock<IRepository> _testRepository;
private readonly Mock<SubmissionVersion> _submissionVersion;
private readonly Note _testNote;
public MyTestClass()
{
   _submissionVersion = new Mock<SubmissionVersion>();
   _testNote = new Note { Id = Guid.NewGuid() };
   _testRepository = new Mock<IRepository>();
   _testRepository.Setup(r => r.GetById<Note>(_testNote.Id).Returns(_testNote);
   _testRepository.Setup(r => r.GetById<SubmissionVersion>(It.IsAny<Guid?>())).Returns(_submissionVersion.Object);
}
[Fact]
public void Should_CallSubmissionVerionNotesRemoveOnce()
{
    // Arrange
    // done is setup
    // Act
    SubmissionVersion.DeleteNote(_testRepository.Object, _submissionVersion.Object, note.Id.Value);
    // Assert
    _submissionVersion.Verify(x => x.Notes.Remove(note), Times.Once());
}
[Fact]
public void Should_CallRepoSaveSubmissionVersionOnce()
{
    // Arrange
    // in setup
    // Act
    SubmissionVersion.DeleteNote(repo.Object, subVersion.Object, note.Id.Value);
    // Assert
    _testRepository.Verify(r => r.Save(_submissionVersion.Object), Times.Once());
}