Let's consider cases, when you may, and when you should not use mocking
framework. Also here I'll show some patterns, you may want to use with
mocking framework.
If you don't care...
Sometimes
class you want to test (system under test, SUT) requires some
dependencies. But it can happen, that your tests do not actually rely
on work of these dependencies. In the case if you are content with
default values returned from all members of a dependency, you may use
mocking framework to create it:
var userSettingsService = new Mock<IUserSettingsService>().Object;
_controller = new SettingsController(userSettingsService);
But even in this case...
... limit usage of the mocking framework
Avoid
situations when each test of your test class contains code, related to
the mocking framework. It will make refactoring of code more difficult.
Try to concentrate all such code inside test initialize and test cleanup
methods.
There are times when it is not possible. For example,
nearly any of your tests may require setting behaviour of some method of
your dependency interface. Here you have several options:
- Configuration method
In your test class you may create helper methods which will help with configuration of expectations:
private void UserIsNotAuthenticated(Mock<IAuthenticationService> authenticationService)
{
authenticationService.Setup(s => s.IsAuthenticated(It.IsAny<HttpRequestBase>())).Returns(false);
}
Now you can use these methods in your tests instead of using the mocking framework directly:
UserIsNotAuthenticated(_authenticationService);
var result = (RedirectResult)_controller.Index(new LogonModel
{
ReturnUrl = FakeUrl
});
Assert.AreEqual(FakeUrl, result.Url);
It will reduce impact of the mocking framework on your tests, and simplify refactoring.
- Preconfigured mocks
Configuration
methods can help if you use them in one test class. But if you need to
define behaviour of some method in several test classes, there is
another possible approach. You can create preconfigured mocks:
internal class AuthenticationServiceStub : Mock<IAuthenticationService>
{
public AuthenticationServiceStub()
{
Setup(s => s.GetUserId(It.IsAny<string>())).Returns(1);
}
public void SetIsPasswordKeyValidTo(bool value)
{
Setup(s => s.IsPasswordKeyValid(It.IsAny<string>())).Returns(value);
}
// other code
}
In
this case, you manually create class of mock, and derive it from class
of the mocking framework. Here you can incapsulate common configuration
for all instances (in constructor), and also define helper methods with
meaningfull names to configure behaviour of separate members.
It is very simple to use such preconfigured mocks:
[TestInitialize]
public void TestInitialize()
{
_authenticationService = new AuthenticationServiceStub();
_controller = new PasswordController(_authenticationService.Object);
}
as well as its configuration methods:
_authenticationService.SetIsPasswordKeyValidTo(false);
var result = (ViewResult)_controller.Index(Constants.Domain, "invalidKey");
Assert.AreEqual("Error", result.ViewName);
Complex behaviour of mocks
There
are cases when you want complex behaviour from your mocks. For
example, you could want several methods of some interface to work
together and provide consistent information. Although it is possible to
achieve it using the mocking framework, I advice you to resist this
temptation. Implement such mocks manually:
It is much cleaner and easier to understand.public class PermissionsProviderStub : IPermissionsProvider
{
internal const string NoPermissions = "NoPermissions";
internal const string ReadPermission = "ReadPermission";
public Permissions GetPermissions(string objectId, int userId)
{
if (objectId.Equals(NoPermissions))
return new NoPermissions();
if (objectId.Equals(ReadPermission))
return new Permissions { HasReadAccess = true };
throw new ArgumentException($"Stub does not suport ObjectId: {objectId} not supported");
}
}
Nevertheless, even in this case the mocking framework can help you...
Partial implementation
There
are interfaces with a very long list of members. I know, that in many
situations we should break up such interfaces into smaller ones. But
still they exist. You may want to implement complex
behaviour for couple of its members, and you don't care about the other
members. In this case, you can use partial implementation of the
interface:
public class LanguageProviderStub : Mock<ILanguageProvider>
{
public LanguageProviderStub()
{
Setup(p => p.GetPreferredLanguage(It.IsAny<int>(), It.IsAny<List<int>>(), It.IsAny<int>()))
.Returns((Func<int, List<int>, int, Lang>)GetPreferredLanguage);
Setup(p => p.GetPreferredLanguageId(It.IsAny<int>(), It.IsAny<List<int>>(), It.IsAny<int>()))
.Returns((Func<int, List<int>, int, int>)GetPreferredLanguageId);
}
private Lang GetPreferredLanguage(int language, List<int> available, int defaultLanguage)
{
if (available.Contains(language))
return new Lang(language);
return new Lang(defaultLanguage);
}
private int GetPreferredLanguageId(int language, List<int> available, int defaultLanguage)
{
var lang = GetPreferredLanguage(language, available, defaultLanguage);
return lang != null ? lang.LangId : -1;
}
}
You assign your implementation to the choosen methods, and make the mocking framework to take care of the rest:
var controller = new LanguageController(new LanguageProviderStub().Object);
Refactoring
I
want to draw your attention to the fact, that using mocking framework
does not mean you should not refactor the code of your tests. You may
start with simple usage of the mocking framework for some interface. But
later, as your tests evolve and grow, you may consider writing
configuration methods and even preconfigured mocks. The way you use the
mocking framework now is not written in stone, it may require changes in
the future.
Conclusion
Let me summarize information in this article.
- Choose your mocking framework wisely.
- Use it if you content with default values of mocked members.
- Limit usage of the framework: use configuration methods and preconfigured mocks.
- Implement complex behaviour manually. In some cases partial implementation is allowed.
- Refactoring is still mandatory.
No comments:
Post a Comment