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
andDbContext
. I don't have enough context to know whatUser
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 thanUserLogic
, which means it should be abstracted by an interface of some sort. I'll assume you already have one which is calledContext
.Therefore, I can assume the implementation of
UserLogic.AddUser
looks like something like this:The outcome that you want to unit test is as follow: If the
User
has already been added to thecontext
, you want to ensure theContext
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 specificUser
. What you want to act on isAddUser
. What you want to assert is that theUser
was not added, and theContext
was not saved. Therefore, your unit test looks like this (in Java, I'm not too familiar with C# testing libraries):As a user of the
UserLogic
class, I can refer to this test to know exactly what the contract ofAddUser
describes in the case where theUser
is already added to the context, which is what I'm looking for in unit tests.