Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reusable Setup for Unit Tests

I am using xUnit and Moq to write my unit tests, and I have a lot of duplicate code in my various tests that I would like to pull out into some reusable fashion.

Duplicate Code

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);

Given the following tests, how can I clean them up so there is not so much duplication?

[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));
}

Any suggestions/patterns that are best practice?

like image 753
Sam Avatar asked Jan 13 '23 01:01

Sam


2 Answers

I typically approach this by making a unit test context object that exposes reusable Mocks as public properties. The object will set up the common mocks internally and just expose them as public properties. You can potentially define many reusable mocks in this class.

Ex:

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);

   }
}

The test can then create an instance:

[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);
}

I prefer this approach over defining the mocks as members in the test class since the UnitTestContext is reusable across test classes.

If you need more flexibility when it comes to return values, you can also pass in objects to the context when you construct the mocks. You can also add to the mocks outside of the class through the Repo property.

like image 130
TGH Avatar answered Jan 18 '23 16:01

TGH


In general, xUnit guys don't like the idea of test setups. But if you need to, you can expose some of the commonly used objects as private members of the test class, and use a parameter-less ctor to initialize. Read attribute comparisons with other testing frameworks.

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());
}
like image 32
Sunny Milenov Avatar answered Jan 18 '23 17:01

Sunny Milenov