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?
2 Answers 2
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.
-
Few extraneous notes in no particular order: I try to follow the
givenX_whenY_thenZ
when naming my tests because I believeCheckUniqueEmail
is not descriptive enough. A class namedUserLogic
raises a flag in my head (as doesContext
). I consider theboolean
return to be a different outcome that what the test I posted asserts, which means I'd expect a second test with the sameArrange
section, but which now asserts that the return of the function isfalse
.Vincent Savard– Vincent Savard2018年02月08日 18:43:55 +00:00Commented Feb 8, 2018 at 18:43
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);
}
addUser
is compromised, it will compromise the whole test.