I'm using xUnit to test my project. I have a test that checks if a user
has been added the a list
of users
shown below:
private readonly IJsonService _jsonService;
private readonly IUserService _userService;
[Fact]
public void Add_User_To_User_List()
{
//Given
_userService = new UserService(_jsonService, _guidService);
_jsonService = Substitute.For<IJsonService>();
var _fakeUserJsonFile = "Users.json";
var _fakeNewUser = new User()
{
ID = new Guid(),
FirstName = "Denis",
LastName = "Menis"
};
var _fakeUserList = new List<User>
{
new User()
{
ID = new Guid(),
FirstName = "Paddy",
LastName = "Halle"
},
new User()
{
ID = new Guid(),
FirstName = "Job",
LastName = "Blogs"
}
};
var _fakeUpdatedUserList = new List<User>
{
new User()
{
ID = new Guid(),
FirstName = "Paddy",
LastName = "Halle"
},
new User()
{
ID = new Guid(),
FirstName = "Job",
LastName = "Blogs"
},
new User()
{
ID = new Guid(),
FirstName = "Denis",
LastName = "Menis"
}
};
_jsonService.DeserializeObject<User>(_fakeUserJsonFile).Returns(_fakeUserList);
_jsonService.SerializeObject(_fakeUserJsonFile, _fakeUpdatedUserList).Returns(true);
//When
var result = _userService.AddUser(_fakeNewUser);
//Then
Assert.Contains(_fakeNewUser, _fakeUpdatedUserList);
}
Now I know that the code works because I wrote it first but when I run my test it fails! Below is my code to add the user to the user list:
public bool AddUser(User user)
{
var userList = GetUsers();
user.ID = _guidService.NewGuid();
userList.Add(user);
var serializeObject = _jsonService.SerializeObject(_fileName, userList);
return serializeObject;
}
GetUser Method:
public List<User> GetUsers()
{
return _jsonService.DeserializeObject<User>(_fileName).ToList();
}
Deserialise Method:
private readonly IFileSystem _file;
private readonly HttpContextBase _httpContext;
private readonly ILogger _logger;
public JsonService(IFileSystem file, HttpContextBase httpContext, ILogger logger)
{
_file = file;
_httpContext = httpContext;
_logger = logger;
}
public IEnumerable<T> DeserializeObject<T>(string fileName)
{
try
{
var relativeFileName = _httpContext.Server.MapPath(fileName);
var readFile = _file.ReadAllText(relativeFileName);
var list = JsonConvert.DeserializeObject<List<T>>(readFile);
return list;
}
catch (Exception ex)
{
_logger.LogException(ex);
return null;
}
}
FileSystem class:
public class FileSystem : IFileSystem
{
public void WriteAllText(string path, string contents)
{
File.WriteAllText(path, contents);
}
public string ReadAllText(string path)
{
return File.ReadAllText(path);
}
}
When I run my test, var serializeObject = _jsonService.SerializeObject(_fileName, userList);
from the AddUser
method returns false every time.
I think it's doing this because even though it's the same data as the expected result, in memory it's a different reference to the same data.
Can anyone help me with this I want it to return the same referenced data. If I'm not being very clear I can elaborate more. Thanks
You're absolutely right that the problem is rooted in the fact that _fakeUserList
and _fakeUpdatedUserList
reference two completely different objects. You've configured _jsonService.SerializeObject
to return true
when it is passed a reference to _fakeUpdatedUserList
- but you're actually passing a reference to (a modified) _fakeUserList
.
Basically, _fakeUpdatedUserList
is completely unnecessary. You can focus on _fakeUserList
, since that is the object that gets provided to the SUT (via DeserializeObject<User>
, presumably).
For example:
[Fact]
public void Add_User_To_User_List()
{
//Given
_userService = new UserService(_jsonService, _guidService);
_jsonService = Substitute.For<IJsonService>();
var _fakeUserJsonFile = "Users.json";
var _fakeNewUser = new User()
{
ID = new Guid(),
FirstName = "Denis",
LastName = "Menis"
};
var _fakeUserList = new List<User>
{
new User()
{
ID = new Guid(),
FirstName = "Paddy",
LastName = "Halle"
},
new User()
{
ID = new Guid(),
FirstName = "Job",
LastName = "Blogs"
}
};
_jsonService.DeserializeObject<User>(_fakeUserJsonFile).Returns(_fakeUserList);
_jsonService.SerializeObject(_fakeUserJsonFile, _fakeUserList).Returns(true); // Match the original _fakeUserList, since that is what gets passed in by the implementation
//When
var result = _userService.AddUser(_fakeNewUser);
//Then
Assert.Contains(_fakeNewUser, _fakeUserList); // Verify that the provided _fakeUserList has been modified
}
On a side note: you can actually remove a lot of the details from this test, as they're irrelevant to the functionality that is being tested. For example, _fakeUserList
can initially be empty - it doesn't have to contain any dummy values. And you can use the default values for _fakeNewUser
(i.e., without specifying FirstName
etc.), as they're not referenced at all in this test.
Edit: Thanks for posting the additional code (for GetUsers
, etc.). This code shows that you're invoking ToList()
on the IEnumerable<User>
that is returned by DeserializeObject<User>
. This is why your mock object is not behaving as you expect: the list returned by ToList()
is a completely separate list from _fakeUserList
.
Additionally, I don't see anywhere in your test where _fakeUserJsonFile
is being injected into the SUT. So the _fileName
in AddUser
might not be what you're expecting, as well.
To get around this, you either need to modify your design (e.g., to not call ToList
), or modify the expected behavior in your test. While you may want to consider the possibility of the former, the latter might be easier.
For example:
// Match any filename (unless you have a way of getting _fakeUserJsonFile into the SUT)
// Match any list, as long as it contains the new user
_jsonService.SerializeObject(Arg.Any<string>(), Arg.Is<List<User>>(list => list.Contains(_fakeNewUser))).Returns(true);
//When
var result = _userService.AddUser(_fakeNewUser);
//Then
Assert.IsTrue(result); // Only returns true if the mock object is invoked as expected
// There is no way to verify the following assertion, because the test has no way of accessing the "updated" list
//Assert.Contains(_fakeNewUser, _fakeUserList);