Monday, January 30, 2017

To Test a Website, You Have To Rebuild Its Interface

If you’re automating tests, then you’re a programmer. What you’re programming is an interface to the application under test.

Lots of people start software test automation the same way — write a test that runs and stands on its own and lets you know whether X thing on a website, given an input of Y, yields Z. Robot type ‘hello’ into Google, robot click button, robot verify results. It’s all right there: the input data, the paths to the site’s elements, the test assertions, all in one script, and life is good for that test case. It’s a tidy little monolith.

If your company desperately needed to know at all times that clicking the ‘Go’ button on Google.com returned search results, you’d be sitting pretty. More likely your company has tasked someone (maybe you) with authoring a mountain of test cases, organized into test suites, to verify all the features of whatever application they’re launching or maintaining. If you start like I started, you automate a handful of these cases in the same way — tiny, monolithic scripts that work — and set each aside, but it starts catching up with you.

You start wrestling with inefficiencies that “real programmers” have been ironing out for years. You’re copying and pasting code for common functions within the application under test. Your UX guy changes the CSS a little and half your scripts can’t find the login text field the following morning. You’re in maintenance hell with your little stable of scripts and you haven’t even automated the smoke test suite yet. What is a test engineer to do?

Don’t panic. What you need is a dose of abstraction.

Consider that any test script you write should be able to be executed on its own, apart from all the other scripts you've written. If your robot needs to sign in to test some protected capability, then signing in should be a component of the test script for that capability. This is important because you may end up realizing that 98.8% of your test cases require the user to be signed in, or to have uploaded a file, or to have accomplished any one of a thousand simple primary actions. Here’s the thing: when the developers of the application wrote that sign-in function, they wrote it once and they put it somewhere where other components of the application could access it. Signing in was such a common use case that to repeat it everywhere that it was needed in the code would have been folly. 

You should follow suit.

You’re a programmer, and you need to learn how to set up functions in your language of choice. It should happen in such a way that wherever your monolithic test scripts have the four or five lines that you've pasted everywhere that tell your robot to sign in with username “foo” and password “bar” they can instead have one line that sends these values to the function and gets back the access it needs:

sign_in(“foo”, “bar”);
verify(robot.is_signed_in);

…and elsewhere:

sub sign_in(string username, string password) {
    robot.send_keys_to_login_field(username);
    robot.send_keys_to_password_field(password);
    return “success”;
}

This is an evolution. This is your code getting mature.

This is the beginning of a design pattern called Page Objects. In a nutshell, all the capabilities that can be performed by the application you’re testing should be in functions abstracted away from your test scripts. Any part of your test scripts that return actual pass/fail values should be kept out of those functions. And the easiest way to organize your functions is by grouping them so that capabilities accomplished on the same page (or frequently used component) in the application under test are packaged together in your functional interface. So our friend sub sign_in() up there might be in a whole other file called Login_Page.xx, saved in a place where your test scripts can find it.

It can seem overwhelming. How will you know which capabilities to program into your interface? The application you’re testing has so many capabilities that it seems you could write interfaces for them all year and never write a single actual test against any of them. The secret is to write the test first.

This is the beginning of a concept called Test-Driven Development, or TDD. To wade backward into the generation of our sign_in test above, you would have written the test script, that first chunk, first. You would have then noticed that sub sign_in() doesn’t exist anywhere in the interface you’re building, and so you would go build that second chunk. And you will do the same for every other function needed by your test scripts to interact with the application under test. The advantage is you’ve already got your test cases written. You know what needs to be accomplished. You can write these as test scripts and fill in the blanks in your interface as you go. At any given time your interface will have exactly as much functionality as it needs to take on what your test scripts throw at it and interact with the application you’re testing.

If any of your tests need you to sign in, you will have a function that does it. And if your UX guy changes the name of the login text field, you will only have to change your code in one place. You’re a programmer — spend less time maintaining your code and more time writing programs that will help your team deliver a quality application.

No comments: