使用MOQ测试存储库

5

我正在尝试使用MOQ来模拟存储库的行为以进行测试。我对MOQ还比较新,所以请耐心等待。

给定以下方法:

public static SubmissionVersion DeleteNote(IRepository repository, SubmissionVersion version, Guid noteId)
{
    Note note = repository.GetById<Note>(noteId);
    version.Notes.Remove(note);
    repository.Save(version);
    repository.Delete(note);
    return repository.GetById<SubmissionVersion>(version.Id);
}

这个测试看起来还可以吗?

[Fact]
public void DeleteNoteV2()
{
    // 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());
    repo.Verify(x => x.Save(subVersion.Object), Times.Once());
    repo.Verify(x => x.Delete(note), Times.Once());

    subVersion.Verify(x => x.Notes.Remove(It.IsAny<Note>()), Times.Once());
}

1
一个很好的测试方法是将代码注释掉,确认测试在预期的位置失败,然后再包含代码并确保通过...这就是TDD如此好的方法,因为它同时作为你编写的测试的验证。 - Jeff B
1个回答

9

你的方法是不错的,但我想要做一些调整。我对你的测试和模拟组件进行了一些更改,如下所示。

单元测试

在你的单元测试中,你的测试方法与你在问题中定义的方法并不完全匹配。

单元测试方法调用:

SubmissionVersion.DeleteNote(repo.Object, subVersion.Object, note.Id.Value);

测试的方法:

public static SubmissionVersion DeleteNote
  (IRepository repository, SubmissionVersion version, Guid noteId)

我假设上述方法是另一个类的一部分,我称之为NotesHelper——并不是用于存储库调用的理想名称,但这只是为了让编译工作。

个人建议将您的单元测试分为两个不同的单元测试。一个用于验证是否调用了所需的方法,另一个用于验证笔记是否已从集合中删除。

此外,我创建了一个fakeSubmissionVersion而不是创建mocked SubmissionVersion。这是因为SubmissionVersion内部的例程可能无法模拟。您也可以进行状态基础测试(更可取),以确保已调用version.Notes.Remove(note);。

[Fact]
public void DeleteNote_Deletion_VerifyExpectedMethodsInvokecOnlyOnce()
{
    // Arrange
    var note = new Note { Id = Guid.NewGuid() };
    var fakeSubmissionVersion = new SubmissionVersion() { Notes = new List<Note>() };
    var repo = new Mock<IRepository>();
    repo.Setup(x => x.GetById<Note>(It.IsAny<Guid>())).Returns(note);

    // Act
    NotesHelper.DeleteNote(repo.Object, fakeSubmissionVersion, note.Id.Value);

    // Assert
    repo.Verify(x => x.GetById<Note>(note.Id), Times.Once());
    repo.Verify(x => x.Save(fakeSubmissionVersion), Times.Once());
    repo.Verify(x => x.Delete(note), Times.Once());
}

上述测试可以通过将每个验证定义为其自己的测试来进一步改进。
[Fact]
public void DeleteNote_Deletion_VerifyDeleteMethodCalledOnlyOnce()

如果测试失败,我们就可以知道哪个方法没有按照预期被调用。有时,像上面那样进行多次验证可能会有问题。例如,如果Save方法没有被调用,你永远不会知道Delete方法是否已经被调用了。这是因为当Save方法没有被调用时,异常已经被抛出,并且测试执行已经终止。
这将导致多个测试,但它们更易读和可维护。当然,你可以将通用代码重构为工厂或助手方法。
[Fact]
public void DeleteNote_RemoveNotes_ReturnsExpectedNotes()
{
    // Arrange
    var note = new Note { Id = Guid.NewGuid() };
    var fakeSubmissionVersion = new SubmissionVersion() { Notes = new List<Note>() { note } };
    var repo = new Mock<IRepository>();
    repo.Setup(x => x.GetById<Note>(It.IsAny<Guid>())).Returns(note);

    // Act
    NotesHelper.DeleteNote(repo.Object, fakeSubmissionVersion, note.Id.Value);

    // Assert
    Assert.AreEqual(0, fakeSubmissionVersion.Notes.Count);
}

额外说明:提供描述性的单元测试方法名称也可以提高单元测试的可读性。


你能为我解释一下这个问题吗:在你的单元测试中,被测试的方法(见下文)与你在问题中定义的方法并不完全匹配。 - Sam
1
@Sam,没问题。关于你的问题“给定以下方法”:public static SubmissionVersion DeleteNote(IRepository repository,SubmissionVersion version,Guid noteId)-我相信这是你正在测试的方法(被测试方法)。但是不清楚你的测试目标类(系统被测试对象(SUT))是什么。你还有... new Mock<SubmissionVersion>(),所以我无法推断出你的SUT,即使你的测试意图很明确。因此,我介绍了NotesHelper(不是理想的选择)作为你的SUT。 - Spock

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