Back to Home

How we test server-side code without mobile clients / Badoo Blog

testing · mobile applications · mobile development · php · server-side · server programming

How we test server-side code without mobile clients


    Badoo is a dating service that is available as a website and mobile applications for major platforms. At the beginning of last year, we globally redesigned the site, as a result of which it turned into a "fat client" and began to work in the same way as mobile applications: call commands on the server and receive responses from it according to the protocol that describes the interaction of the client and server parts. These two parts are made by different developers, and, as a rule, the client part is done after the server part is ready. There is a problem: how can a developer of a new feature make sure that the server part is working correctly if there is no client for it yet and there is nothing to check it on?


    To solve this problem in any server task, integration tests must be written, which I will discuss about in this article.


    What is it and how does it work?


    In our case, these tests are an add-on for PHPUnit, thanks to which the test becomes a client application that accesses the server using the protocol. In this case, it is possible to configure which server we want to access. It could be:


    • developer's platform;
    • “Shot” - a special platform with a “combat” base, on which the code of the created feature is laid out;
    • "Staging."

    In the first case, both the client and the server work within the framework of one PHP process, and in the rest it will be a full-fledged client server, when the test sends requests to other servers.


    Here is an example of a similar test, which checks that the user who presented a gift to another user will see this gift in his profile:


    class ServerGetUserGiftsTest extends BmaFunctionalTestCase
    {
        public function testGiftsSending()
        {
            // Given
            $ClientGiftSender = $this->getLoginedConnection(
                \BmaFunctionalConfig::USER_TYPE_NEW,
                [
                    'app_build' => 'Android',
                    'supported_features' => [
                        \Mobile\Proto\Enum\FeatureType::ALLOW_GIFTS,
                    ],
                ]
            );
            $ClientGiftReceiver = $this->getLoginedConnection();
            $gift_type = 1;
            $gift_add_result = $ClientGiftSender->QaApiClient->addGiftToUser(
                $ClientGiftReceiver->getUserId(),
                $ClientGiftSender->getUserId(),
                $gift_type
            );
            $this->assertGiftAddSuccess($gift_add_result, "Precondition failed: cannot add gift from sender to receiver");
            // When
            $Response = $ClientGiftSender->ServerGetUser(
                [
                    'user_id' => $ClientGiftReceiver->getUserId(),
                    'client_source' => \Mobile\Proto\Enum\ClientSource::OTHER_PROFILE,
                    'user_field_filter' => [
                        'projection' => [\Mobile\Proto\Enum\UserField::RECEIVED_GIFTS],
                    ],
                ]
            );
            // Then
            $this->assertResponseHasMessageType(\Mobile\Proto\Enum\MessageType::CLIENT_USER, $Response);
            $user_received_gifts = $Response->CLIENT_USER['received_gifts'];
            $this->assertArrayHasKey('gifts', $user_received_gifts, "No gifts list at received_gifts field");
            $this->assertCount(1, $user_received_gifts['gifts'], "Unexpected received gifts count");
            $gift_info = reset($user_received_gifts['gifts']);
            $this->assertEquals($ClientGiftSender->getUserId(), $gift_info['from_user_id'], "Wrong from_user_id value");
        }
    }

    Let's take this example in parts.


    Each test inherits from the BmaFunctionalTestCase class - the PHPUnit_Framework_TestCase descendant. It implements several auxiliary methods, the main of which is the ability to obtain a client object through which you can send requests to the server:


    $ClientGiftSender = $this->getLoginedConnection(
        \BmaFunctionalConfig::USER_TYPE_MALE,
        [
            'app_build' => 'Android',
            'supported_features' => [\Mobile\Proto\Enum\FeatureType::ALLOW_GIFTS],
        ]
    );

    Here we can “introduce ourselves” to a specific version of the client with its own set of supported features. After executing this method, we have an object that allows us to send requests on behalf of a registered user using a specific application.


    We take this registered user from a special pool of test users. It has a certain number of "clean" users, i.e. they all have the same initial state. When the getLoginedConnection () method is called in a test, one of these users is selected, and it is blocked for use by other tests. Locking is necessary so that we always deal with users in a condition known to us. After blocking this user, you can carry out any manipulations, and after the test finishes, the cleaning mechanism is launched, which will return the user to the initial "clean" state, and that user will again be available for use in tests. All test users are in the same location, in which there are no real users. Therefore, on the one hand, in tests we are dealing with a predictable environment, and on the other, real users do not see test ones.


    As a rule, we cannot start the scan immediately after receiving the client object: we need to create the environment necessary for the test (in this example, send a gift to another user). We can do this “honestly” by sending requests to the server through the client object, but this is not always possible. In the case of a gift, an “honest” path would be too complicated: we need to replenish the user’s account, get a list of available gifts, send it and wait for it to be processed by the sending script. All this will complicate the test and increase the time of its development and execution.


    To simplify this, we use an internal tool called QaAPI (my colleague Dmitry Marushchenko already talked about it, the presentation and video can be found on Habr ). It consists of many small methods, each of which allows you to perform separate actions on users bypassing standard mechanisms or to get some information about the user. With it, you can add photos to the user and immediately moderate them, bypassing the queues and checking by moderators; change the values ​​of individual fields in his profile, vote for other users in "Dating", etc.


    In this example, we simply give a gift without replenishing the account and bypassing the queues:


    $gift_add_result = $ClientGiftSender->QaApiClient->addGiftToUser(
        $ClientGiftReceiver->getUserId(),
        $ClientGiftSender->getUserId(),
        $gift_type_id
    );
    $this->assertGiftAddSuccess($gift_add_result, "Precondition failed: cannot add gift from sender to receiver");

    It is very important to check the QaAPI responses , because in case of an error the user will be in a completely different state than we expect to receive, and further checks will be pointless. If we talk about our example, it would be strange to check the presence of a gift in the profile if we could not give it.


    If for some reason we do not want to “honestly” bring the user to the desired state, then we can use remote mock-objects . Unlike local ones, they are disposable (acting on only one command) and permanent (working until the end of the test).


    Technically, mock objects are implemented using our other solution, SoftMocks . It is used either directly (on the developer's site, when the test runs as part of a single process), or through the "laying" in the form of memcache (on a remote site). In the second case, during the test, we put information about the new mock-object into an array of disposable or permanent mock-objects, and before sending a request to the server we combine these two arrays and put them in memcache, where the server part can pick them up.


    We often use such mock objects to check tokens when we need to make sure that the text we need comes in the answer. This can be done “honestly”, but it will not be very convenient: the texts can change over time (and this will break the test), plus they can be different in different languages. To avoid these problems, we replace the tokens with some predefined values ​​or even on the way to the texts.


    In general, using mock objects makes the test faster because allows you to get rid of one or more remote calls, but adds dependencies on server code and makes them less reliable: they break more often and lie more.


    After creating the necessary environment, we can send a request to the server and get a response:


    $Response = $ClientGiftSender->ServerGetUser(
        [
            'user_id' => $ClientGiftReceiver->getUserId(),
            'user_field_filter' => [
                'projection' => [\Mobile\Proto\Enum\UserField::RECEIVED_GIFTS],
            ],
        ]
    );

    In such tests, the server code represents a black box for us : we do not know what is happening there and which code is processing our request. All we can do is verify that the server response matches our expectations .


    Our protocol allows the server to return different types of responses to the same command. Commands can return different types of responses. For example, almost any command can return an error. For this reason, we start checking the response with whether there is a message of the expected type there:


    $this->assertResponseHasMessageType(\Mobile\Proto\Enum\MessageType::CLIENT_USER, $Response);

    After we have verified that we have the right message, we can check the answer in more detail and make sure that it contains our gift:


    $user_received_gifts = $Response->CLIENT_USER['received_gifts'];
    $this->assertArrayHasKey('gifts', $user_received_gifts, "No gifts list at received_gifts field");
    $this->assertCount(1, $user_received_gifts['gifts'], "Unexpected received gifts count");
    $gift_info = reset($user_received_gifts['gifts']);
    $this->assertEquals($ClientGiftSender->getUserId(), $gift_info['from_user_id'], "Wrong from_user_id value");

    For commands that modify the state of a user, it is not enough to check the server response . For example, if we send a command to delete a gift, then it is not enough to get Success in the answer - you also need to check that the gift is really deleted. To do this, you can either call other commands and check their answers, or use the same QaAPI by calling a method that returns the state of the parameter we want to check. In the example of deleting a gift, we could call the QaAPI method, which returns a list of gifts and check that it does not have just been deleted.


    What are the advantages?


    The main advantage of such tests is the understanding that the new functionality works as we expect. If we described the scenario in the form of such a test and it passed, then we understand that all the functionality works and can be used by a real client application .


    Another important plus: we can conduct regression testing and make sure that the changes made do not break old customers for whom new functionality will be unavailable. These tests allow us to do this by specifying different versions of the application (this is the old path that we used to version earlier) and a specific set of features supported by the client (this is the new path we are using now).


    What are the disadvantages?


    The main disadvantages of these tests are the long operating time and instability arising from their high level. Although tests usually check the results of one protocol command, they create a full-fledged environment that works with the same databases and services as ordinary clients. All this, as well as an “honest” recreation of the environment, requiring other requests (often not one or two) to the server, takes time.


    Some features require complex initialization , which increases the size of test methods. After all, before calling the test method, you need not only to send requests for initialization, but also to verify that they worked the way you expected. For example, if you want to check the operation of the chat, then you need to get two clients, give them the opportunity to "chat" with each other, send a message and check that it really went. It happens that some things happen with a delay and you need to wait for the delivery of data.


    Because of this complexity, tests become very “fragile”: a failure to recreate the environment will break your test , and although the problem does not apply to what you are testing, your test will fail. Such tests will not tell you what exactly is broken, you only understand that something is not working. You will have to look for a specific method, the change of which broke the test, and sometimes it can be very difficult to do.


    Conclusion


    Despite the listed disadvantages, these tests solve their problems and allow developers to write tests in the same form as unit tests familiar to everyone.


    Viktor Pryazhnikov, Features Developer

    Read Next