4

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('[email protected]');

    userlogic.AddUser(user);

    //Act
    UserLogic userlogicNew = new UserLogic(context);
    User userNew = new User('[email protected]');

    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='[email protected]'}))
context.SaveChanges();    

//Act
UserLogic userlogicNew = new UserLogic(context);
User userNew = new User('[email protected]');

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?

13
  • Are you inserting data into an actual database here? That seems fragile.
    – RubberDuck
    Commented Feb 8, 2018 at 9:57
  • 2
    Forgot to mention, i am using EFCore in memory database for testing Commented Feb 8, 2018 at 10:17
  • 1
    Then #2. Load the preconditions directly on the mocked layer. #1 is assuming that the same method you are testing, is working fine :-). That's too much assumption. If by any reason addUser is compromised, it will compromise the whole test.
    – Laiv
    Commented Feb 8, 2018 at 10:26
  • 2
    @Laiv - right. The key principle here is that, as far as you can reasonably arrange it, one problem in the code should cause one test to fail. If you're depending on your production code to set up data for a test, then the test can fail if that production code is broken, too. If you need to do a lot of setup and doing it via the database is more verbose (or your database design changes a lot), consider adding utilities for making it easier as part of the testing code.
    – Jules
    Commented Feb 8, 2018 at 10:41
  • 1
    You should also consider testing using SQLite in-memory mode. It integrates nicely with EF Core: docs.microsoft.com/en-us/ef/core/miscellaneous/testing, and according to Microsoft, unlike in-memory testing, it will always behave like a relational database.
    – Eternal21
    Commented Feb 8, 2018 at 12:33

2 Answers 2

3

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("[email protected]");
  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.

1
  • Few extraneous notes in no particular order: I try to follow the givenX_whenY_thenZ when naming my tests because I believe CheckUniqueEmail is not descriptive enough. A class named UserLogic raises a flag in my head (as does Context). I consider the boolean return to be a different outcome that what the test I posted asserts, which means I'd expect a second test with the same Arrange section, but which now asserts that the return of the function is false. Commented Feb 8, 2018 at 18:43
2

You should wrap your entity framework calls in a repository class which implements an interface.

You can then inject a mock repository into your userlogic class. with the appropriate already existing user and avoid having a database dependency.

An alternative for where you have complicated data dependencies is to use database snapshots to create an entirely new database populated with dummy data for a single test

IUserRepo
{
    User GetUser(string id);
    void AddUser(User user);
}

MyTest()
{
    //set up mock with the framework of your choice
    IUserRepo repo = new MockUserRepo();
    repo.GetUser = ('[email protected]') => { return new User('[email protected]'); };

    var userLogic = new UserLogic(repo)
    var user = new User('[email protected]');
    var actual = userLogic.AddUser(user);
    Assert.IsFalse(actual);
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.