Unit-testing – Does it matter how I setup test data when creating unit tests

tddtestingunit testing

I have a unit test similar to the code snippet below, it should check that the AddUser method only allows unique emails.

My question is around the Arrange part of this unit test, I use existing system code to setup the first user (class UserLogic), this is so that I have a user in context to perform the next parts of the test (Act and Arrange).

[Fact]
public void CheckUniqueEmail()
{
    var context = new DbContext(); //EF Core in memory db

    //Arrange
    UserLogic userlogic = new UserLogic(context);
    User user = new User('test@test.com');

    userlogic.AddUser(user);

    //Act
    UserLogic userlogicNew = new UserLogic(context);
    User userNew = new User('test@test.com');

    bool result = userlogicNew.AddUser(userNew); //result should be false since this email has already been used

    //Assert
    Assert.False(result);
}

However, I have seen this done in two ways: The first is as I have done above. The second would be to insert data directly into context, as in the next example

[Fact]
public void CheckUniqueEmail()
{
var context = new DbContext(); //EF Core in memory db

//Arrange
context.Users.Add(new User({Email='test@test.com'}))
context.SaveChanges();    

//Act
UserLogic userlogicNew = new UserLogic(context);
User userNew = new User('test@test.com');

bool result = userlogicNew.AddUser(userNew); //result should be false since this email has already been used

//Assert
Assert.False(result);
}

Based on the foregoing, does matter the way I arrange the data for the unit tests? Which one of the two approaches do you think is appropriated for unit tests?

Best Answer

How you set up test data is important, and I'd argue both versions are suboptimal.

Every method or class you write has a contract, whether explicit (by documentation for instance) or implicit (by what the code actually does). This contract is important, because it describes what the clients, i.e. the code that uses your class, should expect when it uses it. Unit testing is a method to programmatically document the contract of the code under test, in a way that it ensures that the behaviour is the same even if the implementation changes.

An important characteristic of unit tests is that they want the code under test (CUT) to be isolated from other code. This means you have to be very careful when the CUT has dependencies. If it uses a dependency which has a different reason to change than itself (this is what responsibility means in the single responsibility principle), you'll usually have an abstraction to isolate these two. This abstraction itself has a contract, and in unit testing, you assume that the abstraction on which you depend will behave according to its contract. In unit tests, this generally means that this dependency will be mocked, and you will pilot the mock to behave in a certain way.

Back to your example now. You are unit testing the UserLogic class. It has two dependencies that I can see: User and DbContext. I don't have enough context to know what User is, but I'll assume it is some sort of value object. In that case, it is fine to use it directly.
DbContext is a different beast. It seems to be an implementation of some sort of persistence. It definitely has a different reason to change than UserLogic, which means it should be abstracted by an interface of some sort. I'll assume you already have one which is called Context.

Therefore, I can assume the implementation of UserLogic.AddUser looks like something like this:

public boolean AddUser(User user) {
  if (context.HasUser(user)) {
    return false;
  }

  context.AddUser(user);
  context.SaveChanges();
  return true;
}

The outcome that you want to unit test is as follow: If the User has already been added to the context, you want to ensure the Context hasn't changed (no new users were added, nor were the changes saved).

The description of the outcome described pretty much exactly how the unit test should look. What you want to arrange is that the context already has a specific User. What you want to act on is AddUser. What you want to assert is that the User was not added, and the Context was not saved. Therefore, your unit test looks like this (in Java, I'm not too familiar with C# testing libraries):

@Test
public void givenContextAlreadyHasTheUser_whenAddUser_thenTheUserIsNotAddedASecondTime() {
  // Arrange
  Context context = mock(Context.class);
  User user = new User("test@test.com");
  UserLogic userLogic = new UserLogic(context);
  given(context.HasUser(user)).willReturn(true);

  // Act
  userLogic.AddUser(user);

  // Assert
  verify(context, never()).AddUser(any());
  verify(context, never()).SaveChanges();
}

As a user of the UserLogic class, I can refer to this test to know exactly what the contract of AddUser describes in the case where the User is already added to the context, which is what I'm looking for in unit tests.