📜 ⬆️ ⬇️

Moki and stubs

There is a category of classes that are very easy to test. If a class depends only on primitive data types and has no connection with other business entities, then it is enough to create an instance of this class, “kick” it in some way by changing a property or calling a method and check the expected state.

This is the easiest and most effective way to test, and any sensible design is repelled by similar classes, which are the “building blocks” of the lower level, on the basis of which more complex abstractions are then built. But the number of classes that live in such “isolation” is not much in nature. Even if we normally selected all the logic for working with the database (or service) into a separate class (or set of classes), sooner or later someone will appear who will use these classes to get higher-level behavior and this This will also need to be tested.

But first, let's consider a more typical case when the logic for working with a database or an external service, as well as the logic for processing this data is concentrated in one place.

')
//  ,     //    public class LoginViewModel { public LoginViewModel() { //     UserName = ReadLastUserName(); } //  ;     public string UserName { get; set; } //   UserName public void Login() { //      ,    // .         //  SaveLastUserName(UserName); } //      private string ReadLastUserName() { //  ,       ... //   -,     return "Jonh Doe"; } //     private void SaveLastUserName(string lastUserName) { //  ,   ,    } } 


When it comes to testing such classes, this view model is usually placed on a form that is then tested by hands. If instead of a view model, this mixing of logic occurs during the implementation of server components, then they are tested by creating a simple console application that will call the necessary high-level functions, testing the entire module in this way. In both cases, this test option can not be called very much automatic.

NOTE
You do not need to throw stones at me with shouts “But who today can even write such crap? After all, so much has been written about the dangers of such an approach, and indeed, we have a unit-shmuniti and other utilities, so this is an unreal accordion twenty years ago! ” By the way, yes, this is a button accordion, but, first, it’s not about units and other containers, but about basic principles, and secondly, such “integration” testing is still incredibly popular, at least among many of my “foreign” ones colleagues.

Creating “stitches” to test the application


Even if you do not think about how many new-fashioned design principles violate our view model, it is clearly seen that its design is somewhat ... wretched. After all, even if you design it with the old grandfather’s Buchev method, it becomes clear that all the work to preserve the last user’s name, the logic of working with the database (or other external data source) needs to be hidden out of sight and make it a “problem” another and use this “someone” as a “building block” to get higher level behavior:

 internal class LastUsernameProvider { //         public string ReadLastUserName() { return "Jonh Doe"; } //   ,       public void SaveLastUserName(string userName) { } } public class LoginViewModel { //          private readonly LastUsernameProvider _provider = new LastUsernameProvider(); public LoginViewModel() { //        UserName = _provider.ReadLastUserName(); } public string UserName { get; set; } public void Login() { //         //    _provider.SaveLastUserName(UserName); } } 


So far, writing a unit test is still difficult, but it becomes clear how you can simply “fake” a real implementation of the LastUsernameProvider class and simulate the behavior we need. It is enough to select the methods of this class in a separate interface, or simply to make them virtual and override them in successor. After that, it remains only to “screw” the object we need into our twist model.

NOTE

Honestly, I'm not a big fan of design changes just for the sake of code “testability”. As practice shows, a normal OO design is either already sufficiently “tested” or it only requires minimal gestures to make it so. Some additional thoughts about this can be found in the article “Ideal architecture” .

Even without resorting to any third-party libraries for “injecting” dependencies, we can do this on our own in several simple ways. The required dependency can be passed through an additional constructor, through a property, or create a factory method that will return the ILastUsernmameProvider interface.

Let's look at the version with the designer, which is quite simple and popular (with a small number of external dependencies, it works just fine).

 //     internal interface ILastUsernameProvider { string ReadLastUserName(); void SaveLastUserName(string userName); } internal class LastUsernameProvider : ILastUsernameProvider { //         public string ReadLastUserName() { return "Jonh Doe"; } //   ,       public void SaveLastUserName(string userName) { } } public class LoginViewModel { private readonly ILastUsernameProvider _provider; //       public LoginViewModel() : this(new LastUsernameProvider()) {} // ""        "" internal LoginViewModel(ILastUsernameProvider provider) { _provider = provider; UserName = _provider.ReadLastUserName(); } public string UserName { get; set; } public void Login() { _provider.SaveLastUserName(UserName); } } 


Since the additional constructor is internal, it is available only inside this assembly, as well as the "friendly" assembly of unit tests. Of course, if the tested classes are internal, there will not be any, but since all the “clients” of the internal class are in the same assembly, then it is easier to control them. Such an approach, based on the addition of an internal method for setting "fake" behavior, is a reasonable compromise to simplify code testing without imposing restrictions on the use of more complex dependency management mechanisms, such as IoC containers.

NOTE
One of the drawbacks when working with interfaces is the drop in readability, since it is not clear how many interface implementations exist and where the implementation of one or another interface method is located. Tools such as Resharper substantially mitigate this problem, since they support not only the navigation to the method declaration (Go To Declaration), but also the navigation to the implementation of the method (Go To Implementation):




Status check vs behavior check


Now let's try to write a unit test first for the LoginViewModel class constructor , which receives the name of the last logged in user, and then the unit test for the Login method, after which the last user name should be saved.

For the normal implementation of these tests, we need a “fake” interface implementation. In the first case, we need to return an arbitrary name of the last user in the ReadLastUserName method, and in the second case - to make sure that the SaveLastUserName method is called .

This is where the two types of fake classes differ : stubs are designed to get the desired state of the test object , and mocks are used to check the expected behavior of the test object .

Stubs are never used in statements, they are simple "servants" who only simulate the external environment of the test class; at the same time, the statements check the state of the test class, which depends on the established state of the stub.

 //       internal class LastUsernameProviderStub : ILastUsernameProvider { //   ,     //      public string UserName; //     -   UserName public string ReadLastUserName() { return UserName; } //         public void SaveLastUserName(string userName) { } } [TestFixture] public class LoginViewModelTests { //        - [Test] public void TestViewModelConstructor() { var stub = new LastUsernameProviderStub(); // ""   stub.UserName = "Jon Skeet"; // -!! var vm = new LoginViewModel(stub); //     Assert.That(vm.UserName, Is.EqualTo(stub.UserName)); } } 


Mocks have a different role. Mocks “slip” the object under test, but not to create the required environment (although they can fulfill this role), but first of all so that later you can check that the object under test has performed the required actions . (That is why this type of testing is called behavior testing , unlike stubs, which are used for state - based testing ).

 //   ,   SaveLastUserName   //    internal class LastUsernameProviderMock : ILastUsernameProvider { //           public string SavedUserName; //          , //    ""    "" public string ReadLastUserName() { return "Jonh Skeet";} //          SavedUserName  public void SaveLastUserName(string userName) { SavedUserName = userName; } } // ,     Login      [Test] public void TestLogin() { var mock = new LastUsernameProviderMock(); var vm = new LoginViewModel(mock); //   -     vm.UserName = "Bob Martin"; //     Login vm.Login(); //   ,     SaveLastUserName Assert.That(mock.SavedUserName, Is.EqualTo(vm.UserName)); } 


And why should I know about these differences?


Indeed, the difference in concepts may seem insignificant, especially if you are implementing these "fakes" with your hands. In this case, knowledge of these patterns will only allow you to speak with other developers in the same language and simplify the name of the fake classes.

However, sooner or later, you can get bored with this wonderful exercise in the manual implementation of interfaces and you turn your attention to one of the Isolation frameworks, such as Rhino Mocks, Moq or Microsoft Moles. There, these terms will necessarily meet and an understanding of the differences between these types of fakes will be very useful for you.

I deliberately did not touch on any of these frameworks, since each of them deserves a separate article and IMO will only complicate the understanding of these concepts. But if you are still interested to look at some of these frameworks in more detail, then I wrote about one of them in more detail: “Microsoft Moles” .

Source: https://habr.com/ru/post/134836/


All Articles