Skip to main content

Test Automation Heuristic: No Conditionals

A conditional statement is the use of if() (or its relatives) in code. Not using if() is a fairly well-known test design principle. I will explain why that is, but I am also going to point out some less well-known places where it is tempting to use if(), but where other flow constructs are better choices. My code looks like Ruby for convenience, but these principles are true in any language.

No Conditionals in Test Code

This is the most basic misuse of if() in test code:

if (x is true)
  test x
elsif (y is true)
  test y
  do whatever comes next

It is a perfectly legal thing to do, but consider: if this test passes, you have no way of knowing which branch of the statement your test followed. If "x" or "y" had some kind of problem, you would never know it, because your conditional statement allows your test to bypass those situations.

Far better is to test x and y separately: 
def test_x
  (bring about condition x)
  (do an x thing)
  (do another x thing)

def test_y
  (bring about condition y)
  (do a y thing)
  (do another y thing)

No Conditionals in Framework Code

It is also good to avoid conditionals in other parts of your test framework. One example in my own framework is where I choose which browser to run my tests for. It is tempting to do something like

if ENV['SELENIUM_BROWSER'] = 'firefox'
  caps.platform = 'Windows 10'
  caps.version = '53.0'
elsif ENV['SELENIUM_BROWSER'] = 'chrome'
  caps.platform = 'Windows 10'
  caps.version = '58.0'

In this instance, I think that a case() statement is not only more readable, but also more definitive as to what the environment variable options must be. Although a case() statement may have 'else' as an option, instead of falling through a series of elsif() conditions to the end of the chain, a case() statement directs the code exactly where you want it to be.

        when 'internet_explorer'
          caps.platform = 'Windows 10'
          caps.version = '11.103'
        when 'chrome'
          caps.platform = 'Windows 10'
          caps.version = '58.0'
        when 'firefox'
          caps.platform = 'Windows 10'
          caps.version = '53.0'
          puts 'Environment variable SELENIUM_BROWSER is not valid'

No Conditionals for Irrelevant Conditions

From time to time, test automation has to accommodate local conditions that have no bearing on the test itself. When this happens, it is tempting to say "if (stuff is in the way)...". In these cases, I prefer a try/catch operation (in Ruby, this is begin/rescue) over a conditional. I have a couple of examples:

One system I work in has an intermittent bug. I can't fix this bug, it is part of a third-party framework my application uses, and my team has no access to the source code, nor do we have influence with the people providing the framework. I have to work around it. Rather than using a conditional to check if the bogus condition is in place before handling it, I just attempt to handle it, and if the attempt fails, I move on. The code (shown as a step in Cucumber) looks like this:
When(/^I click the Action button$/) do

As another example, I work in test environments where it is impossible to delete a User. Instead of using a conditional to check if the User record I need is in place before the setup steps create it, I just attempt to create the record, and if I fail, print an informative message and move on:

    create_user_via_api("Test_user #{@random_string}"
    puts 'User already exists'

In my system, this is actually more efficient than using a conditional to check for the existence of the User record I need.

No Conditionals When Controlling the Test Environment

One last pattern I follow is to treat my test environments in a REST-like fashion.  REST stands for "Representational State Transfer", and it is used to describe a particular approach to managing software systems.

In a REST-like approach, you have a client and a server. The client sends a message to the server saying in essence "Be the way I want you to be". The server may return one of several responses, saying "OK" or "You can't do that" or "I can't do that" (In HTTP, these are respectively 200, 400, and 500 response codes. )

We can adopt the same approach when managing our test environments. One example from my own experience is that I have to manage test environments where, when I first encounter the system, a drop-down list of applications exists containing a number of options that are not of interest, and one option that is the application I want to test.

To simplify, let's say my drop-down list contains options for "Foo App", "My App", and "Bar App". Let's also say that the default choice in a fresh test environment is "Foo App", but in a test environment I have already used, I have already chosen "My App", and it becomes the default choice after that.

A naive use of a conditional would do something like
if app == "Foo App"
  select "My App"

But if for some reason the default option is set to "Bar App", then this code breaks down.

A less naive use of a conditional would do something like
if app != "My App"
  select "My App"

This is better, because it does not matter what wrong choice is the default, the code will always select "My App" if it is not already selected.

But that is not how I prefer to manage my test environments. In this case I would simply "select "My App"" every time I hit any instance of this system. In a REST-like approach, I tell the system to be the way I want it to be regardless of what state it is in before I encounter it.

There is a valid argument that this is less efficient than the "if app != "My App"" approach, but I do this because there are many things I may not know about the state of any particular instance of this environment before my test code begins manipulating my application. Always starting by selecting "My App" forces the system to call the server and refresh the page with the latest conditions, regardless of what state the page may be in when I first encounter it, regardless of what my test code may have done before it starts to manipulate my application. This REST-like approach to system state guarantees the most consistent behavior for the automated tests that follow.