Writing Autotests Effectively - Subcutaneous tests

Let's imagine a hypothetical sitauation (into which we regularly plunge). You have been assigned to a project to “gash” automation. You are given a huge test plan with a large number (thousands of them!) Of “manual” tests, and they say that you need to do something, and right there. And also, to quickly and stably.

It’s too late to write Unit tests, or even think about TDD , the product code has long been written. Your word, comrade autotester!

image

Fortunately, there is a small trick that will allow you to increase coverage and make the tests stable and fast - Subcutaneous tests (“subcutaneous tests”), but first things first.


The essence of the problem


The first conditional reflex of the automator is to take Selenium (well, either there, Selenide, or some other prodigy for UI tests). This is such an industry standard, but there are many reasons why it doesn’t take off:

  • UI tests are slow. There is no escape from this. They can be run in parallel, filed and done a little faster, but they will remain slow.
  • UI tests are unstable. Partly because they are slow. And also because the Web browser and user interface were not created to be controlled by a computer (this trend is currently changing, but not the fact that this is good).
  • UI- — . . ( , , , «» - , ).
  • , , , UI- . . ID XPath . , «» - - . , , — - , .
  • Someone will say that some functionality simply cannot be tested otherwise. I will say that if there is functionality that can be tested only by UI tests (with the exception of the UI logic itself), this can be a good sign of architectural problems in the product.

The only real plus of UI tests is that they allow you to “throw” more or less useful checks without the need to dive and study the code of the product itself. Which is hardly a plus in the long run. A more detailed explanation of why this can be heard in this presentation .

Alternative solution


As a very simple case, let's consider an application consisting of a form where you can enter a valid username. If you enter a user name that matches the rules - User will be created in the system and recorded in the Database.



The source code of the application can be found here: github.com/senpay/login-form . You were warned - in the application there are a lot of bugs and there are no fashionable tools and frameworks.

If you try to "throw" a check sheet for this application, you can get something like:
NumberStepsExpected results
11. Enter a valid user name
2. Click the "Log in" button
1.
2. A new user is created.
21. Enter an empty user name
2. Click the "Log in" button
1.
2. The error message is given.

Does it look simple? Simply! Can I write UI tests? Can. An example of the written tests (along with a full three - level framework ) can be found in LoginFormTest.java if you go to the uitests label in git ( git checkout uitests ):

public class LoginFormTest {

    SelenideMainPage sut = SelenideMainPage.INSTANCE;
    private static final String APPLICATION_URL = "http://localhost:4567/index";

    @BeforeClass
    public static void setUpClass() {
        final String[] args = {};
        Main.main(args);
        Configuration.browser = "firefox";
    }

    @Before
    public void setUp() {
        open(APPLICATION_URL);
    }

    @After
    public void tearDown() {
        close();
    }

    @Test
    public void shouldBeAbleToAddNewUser() {
        sut.setUserName("MyCoolNewUser");
        sut.clickSubmit();
        Assert.assertEquals("Status: user MyCoolNewUser was created", sut.getStatus());
        Assert.assertTrue(sut.getUsers().contains("Name: MyCoolNewUser"));
    }

    @Test
    public void shouldNotBeAbleToAddEmptyUseName() {
        final int numberOfUsersBeforeTheTest = sut.getUsers().size();
        sut.clickSubmit();
        Assert.assertEquals("Status: Login cannot be empty", sut.getStatus());
        Assert.assertEquals(numberOfUsersBeforeTheTest, sut.getUsers().size());
    }
}


Some metrics for this code:
Runtime: ~ 12 seconds (12 seconds 956 milliseconds the last time I ran these tests)
Code coverage
Class: 100%
Method: 93.8% (30/32)
Line: 97.4% (75/77 )

Now let's assume that Functional AutoTests can be written at the level “immediately below” the UI. This technique is called Subcutaneous tests (“subcutaneous tests” - tests that test immediately below the level of display logic) and was proposed by Martin Fowler quite a long time ago [ 1 ].

When people think of “non-UI” autotests, often they think immediately of REST / SOAP or its API. But the API (Application Programming Interface) is a much broader concept, not necessarily affecting HTTP and other heavyweight protocols.

If we pick a product code , we can find something interesting:
public class UserApplication {

    private static IUserRepository repository = new InMemoryUserRepository();
    private static UserService service = new UserService(); {
        service.setUserRepository(repository);
    }

    public Map<String, Object> getUsersList() {
        return getUsersList("N/A");
    }

    public Map<String, Object> addUser(final String username) {
        final String status = service.addUser(username);
        final Map<String, Object> model = getUsersList(status);
        return model;
    }

    private Map<String, Object> getUsersList(String status) {
        final Map<String, Object> model = new HashMap<>();
        model.put("status", status);
        model.put("users", service.getUserInfoList());
        return model;
    }
}


When we click on a UI, one of these methods is called, or a new User object is added, or a list of already created User objects is returned. What if we use these methods directly ? After all, this is a real API! And most importantly, REST and other APIs also work on the same principle - they call a certain method of "controller level".

Using these methods directly, we can write a simpler and better test:
public class UserApplicationTest {

    private UserApplication sut;

    @Before
    public void setUp() {
       sut = new UserApplication();
    }

    @Test
    public void shouldBeAbleToAddNewUser() {
        final Map<String, Object> myCoolNewUser = sut.addUser("MyCoolNewUser");
        Assert.assertEquals("user MyCoolNewUser was created", myCoolNewUser.get("status"));
        Assert.assertTrue(((List) myCoolNewUser.get("users")).contains("Name: MyCoolNewUser"));
    }

    @Test
    public void shouldNotBeAbleToAddEmptyUseName() {
        final Map<String, Object> usersBeforeTest = sut.getUsersList();
        final int numberOfUsersBeforeTheTest = ((List) usersBeforeTest.get("users")).size();
        final Map<String, Object> myCoolNewUser = sut.addUser("");
        Assert.assertEquals("Login cannot be empty", myCoolNewUser.get("status"));
        Assert.assertEquals(numberOfUsersBeforeTheTest, ((List) myCoolNewUser.get("users")).size());
    }
}


This code is available under the label subctests :

git checkout subctests


Let's try to collect metrics?
Time to execute: ~ 21 milliseconds
Code coverage :
Class: 77.8%
Method: 78.1 (30/32)
Line: 78.7 (75/77)

We lost a little coverage, but the speed of tests increased 600 times !!!

How important / significant is the loss of coverage in this case? Depends on the situation. We lost a little glue code, which may be (or may not be) important (I recommend determining which code is lost as an exercise).

Does this loss of coverage justify the introduction of heavyweight testing at the UI level? It also depends on the situation. We can, for example:
  • Add one UI test to check the glue code, or
  • If we do not expect frequent changes to the glue code - leave it without autotests, or
  • If we have some kind of “manual” testing, there is a great chance that problems with the glue code will be noticed by the tester, or
  • Come up with something else (same Canary deployment)


Eventually


  • Functional autotests are not required to be written at the UI or REST / SOAP API level. The use of “Subcutaneous tests” in many situations will test the same functionality with greater speed and stability.
  • One of the disadvantages of the approach is a certain loss of coverage.
  • One way to avoid losing coverage is the “ Feature Tests Model
  • But even with the loss of coverage, the increase in speed and stability is significant.


An English version of the article is available here .

All Articles