4 rules to improve your test automation code

public://webform/writeforus/profile-pictures/2016-10-28_16.10.45_0.jpg
Nikolay Advolodkin, Senior Test Automation Engineer, Ultimate QA

Test automation is ridiculously hard. Whether you're automating at the system, integration, or unit level, it's tough from beginning to end. You don't know how to get started. And when you start, you don't know how to proceed.

Wouldn't it be awesome if there were some simple rules that helped you write better automation code? The good news is that such rules actually exist. Here are four easy-to-follow tenets that will dramatically improve your test automation code.

World Quality Report 2018-19: The State of QA and Testing

1. Validate that your test will actually fail

This is critical to ensure that your test automation is actually doing its job. Meaning: Is your test automation actually catching bugs and preventing regressions in the application under test (AUT)? Are your automated tests actually validating that the system behaves according to requirements?

Sadly, beginners in test automation rarely validate that their assertions will actually fail correctly. I have seen even seasoned developers write code that will simply never fail. Therefore, as you are writing your automated test, make sure that the expected failure condition will actually elicit the behavior that you expect.

To enforce this rule, you can simply place a breakpoint in your code on the assertion line. Execute your code until that breakpoint. Force the failure condition to occur in your AUT, and then step over the line of code with that assertion. If it fails appropriately, your automated test is actually doing its job, and so are you.

2. Don't repeat yourself 

As Agile Manifesto co-author Robert C. Martin said, "Duplication is the primary enemy of a well-designed system." Few other bad coding practices will destroy your code faster than duplication. Avoiding duplication must be one of your primary concerns as you are writing test automation code. You must attack duplication from all levels.

Duplication can be simply a few lines of code that consistently repeat. For example, you may have multiple tests that do something such as this:

[TestMethod]
[Description("Validate that user is able to fill out the form successfully using valid data.")]
public void Test1()
{
Driver = GetChromeDriver();
SampleAppPage = new SampleApplicationPage(Driver);
TheTestUser = new TestUser();
TheTestUser.FirstName = "Nikolay";
TheTestUser.LastName = "BLahzah";
EmergencyContactUser = new TestUser();
EmergencyContactUser.FirstName = "Emergency First Name";
EmergencyContactUser.LastName = "Emergency Last Name";
SetGenderTypes(Gender.Female, Gender.Female);
SampleAppPage.GoTo();
SampleAppPage.FillOutEmergencyContactForm(EmergencyContactUser);
var ultimateQAHomePage = SampleAppPage.FillOutPrimaryContactFormAndSubmit(TheTestUser);
AssertPageVisible(ultimateQAHomePage);
}
[TestMethod]
[Description("Fake 2nd test.")]
public void PretendTestNumber2()
{
Driver = GetChromeDriver();
SampleAppPage = new SampleApplicationPage(Driver);
TheTestUser = new TestUser();
TheTestUser.FirstName = "Nikolay";
TheTestUser.LastName = "BLahzah";
EmergencyContactUser = new TestUser();
EmergencyContactUser.FirstName = "Emergency First Name";
EmergencyContactUser.LastName = "Emergency Last Name";
SampleAppPage.GoTo();
SampleAppPage.FillOutEmergencyContactForm(EmergencyContactUser);
var ultimateQAHomePage = SampleAppPage.FillOutPrimaryContactFormAndSubmit(TheTestUser);
AssertPageVisibleVariation2(ultimateQAHomePage);
}

 

There is plenty of duplication in these automated tests. If anything changes, such as the type of driver that you want, or the name of the class representing the SampleApplicationPage, or even how you initialize each of the TestUser objects, you will be in trouble. In the above example, you will need to update two places for any kind of change. This implies double the work and a higher chance of breaking something.

Whenever you encounter such an issue, the obvious solution is to simply wrap the common lines of code in a method. In this case, because they are setup steps, we can also tag that method with a [TestInitialize] attribute and let our testing framework call the method before every [TestMethod].

For example:

        [TestInitialize]
        public void SetupForEverySingleTestMethod()
        {
            Driver = GetChromeDriver();
            SampleAppPage = new SampleApplicationPage(Driver);
            TheTestUser = new TestUser();
            TheTestUser.FirstName = "Nikolay";
            TheTestUser.LastName = "BLahzah";
            EmergencyContactUser = new TestUser();
            EmergencyContactUser.FirstName = "Emergency First Name";
            EmergencyContactUser.LastName = "Emergency Last Name";
        }
This method will now be called prior to every single test that you have. Take a look at the drastic improvement.

        [TestMethod]
        [Description("Validate that user is able to fill out the form successfully using valid data.")]
        public void Test1()
        {
            SetGenderTypes(Gender.Female, Gender.Female);
            SampleAppPage.GoTo();
            SampleAppPage.FillOutEmergencyContactForm(EmergencyContactUser);
            var ultimateQAHomePage = SampleAppPage.FillOutPrimaryContactFormAndSubmit(TheTestUser);
            AssertPageVisible(ultimateQAHomePage);
        }
        [TestMethod]
        [Description("Fake 2nd test.")]
        public void PretendTestNumber2()
        {
            SampleAppPage.GoTo();
            SampleAppPage.FillOutEmergencyContactForm(EmergencyContactUser);
            var ultimateQAHomePage = SampleAppPage.FillOutPrimaryContactFormAndSubmit(TheTestUser);
            AssertPageVisibleVariation2(ultimateQAHomePage);
        }

The tests now are much smaller in size, are cleaner, and have less duplicate code.

Duplication that occurs at the class level must also be removed. For example, you may have a method that checks that a specific page has loaded. A standard way to do this might be to check the correct HTTP status code. Afterwards, you may want to check that the URL contains a specific string that is related to that page.

If these validation points are happening in multiple classes, you must remove that duplication. The easiest way to remove such duplication is to move the common code out to a parent class. The parent class may contain a method that performs these two operations so that the single classes don't have to duplicate the code. If the single classes require some special string to be validated, the parent class can force the child classes to implement an abstract property that will be specific to that class.

Removal of duplication is paramount to the success of your test automation code. Always take time to analyze your tests for signs of duplication and apply the strategies above to remove it from your code. 

[ Webinar: Agile Portfolio Management: Three best practices ]

3. Keep functions small

"The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that."
—Robert C. Martin 

Although this seems like a weird rule to have for improving test automation code, it's a rule that is truly powerful. When you create small functions, several things are achieved. First, the functions will be easier to name. Second, the functions will be easier to understand. Third, the functions will more closely adhere to the Single Responsibility Principle.

Let's take a look at an example of a method name:

public static string GetLanguageInUse(string content, string expectedLanguage = "")
{
    string language = string.Empty;
    content = content.Trim();
    var factory = new RankedLanguageIdentifierFactory();
    string directory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + @"\TestData\Core14.profile.xml";
    var identifier = factory.Load(directory);
    var languages = identifier.Identify(content);
    var mostCertainLanguage = languages.FirstOrDefault();
    if (mostCertainLanguage.Item1.Iso639_3 == "eng")
        language = "English";
    else if (mostCertainLanguage.Item1.Iso639_3 == "spa")
        language = "Spanish";
    if (language != expectedLanguage && expectedLanguage != "")
    {
        var letterOverrideLanguage = "";
        if (content.Length == 1 && content.All(char.IsLetter))
        {
            letterOverrideLanguage = expectedLanguage;
        }
        var mathOverrideLanguage = OverrideMathLanguage(content, expectedLanguage);
        var overrideLanguage = OverrideDetectedLanguage(content, expectedLanguage);
        if (letterOverrideLanguage != "")
            language = letterOverrideLanguage;
        else if (mathOverrideLanguage != "")
            language = mathOverrideLanguage;
        else if (overrideLanguage != "")
            language = overrideLanguage;
    }
    return language;
}
If you look at the method name and signature, you will assume that, based on the content that you pass in, it will detect the language being used. Seems reasonable. However, when you take a look inside, you're definitely confused.

This method creates a factory, concatenates some XML directory, identifies the language, determines the mostCertainLanguage and whether it's in English or Spanish, and then finishes with some complicated overriding. Does this method actually get the language in use as advertised? You probably have no clue.

Now look at this method, refactored and smaller:

private static LanguageType GetLanguageInUse(string content, LanguageType expectedLanguage = LanguageType.NoLanguageDetermined)
{
    content = content.Trim();
    var language = DetermineLanguage(content);
    if (language != expectedLanguage && expectedLanguage != LanguageType.NoLanguageDetermined)
        language = OverrideUnexpectedLanguage(content, expectedLanguage);
    return language;
}
It uses the same method name that takes in content and the expected language, which defaults to "no language determined." Inside, we see a string of content is trimmed (Trim()). Then, using that content, a language is determined. Finally, if the determined language does not meet the conditions in the if statement, that language is overridden using the expected language.

What a difference, wouldn't you say? In fact, it's almost pleasant to read such a concise method. And here is another, even better example of how powerful a small method can be:

        public static bool IsContentInEnglish(string content)
        {
            return GetLanguageInUse(content, LanguageType.English) == LanguageType.English;
        }
This method is so elegant. When reading the method signature, you can assume that it returns a Boolean result that lets you know whether the content is in English. Then, when you look inside, you see exactly what you expect. The method uses our previous example to determine the current language and then evaluates it to see if it is equal to English. That's it. 

Having small methods of fewer than 10 lines will allow your code to read like a story. Naming the methods will be easier because the method probably does only a single thing. When your peers look inside your perfectly named method, they will be pleasantly unsurprised by the code that they find there. Now that's a sign of a true code craftsman.

4. Write code only for the current requirements

This rule is a way to tell you to stop over-engineering. Over-engineering usually occurs as a result of a developer attempting to protect his or her code from future changes that may or may not occur. 

With overengineering, the big problem is that it makes it difficult for people to understand your code. There's some piece built into the system that doesn't really need to be there, and the person reading the code can't figure out why it's there, or even how the whole system works (since it's now so complicated).
Code Simplicity: The Fundamentals of Software

There are many different ways to over-engineer your test code. One example is trying to create code that can handle all future scenarios. I've seen test automation engineers try to automate an entire page object before they even need all of the elements. They will create a class that represents an HTML page. However, rather than simply adding properties and methods that they require for the current test, they will add all actions and locators to their class.

This is a horrible mistake because they can't be sure if they will ever use that code. Nobody can be sure. So the end result is that they create giant classes that have very little usable code. Anyone interacting with that code must navigate through useful and useless methods and properties. This is a pointless exercise that simply wastes time.

The best way to prevent yourself from writing complicated code is to simply follow the current requirements. If you need to test whether a method is returning the appropriate string, then simply write a test for that. Do not try to test for future conditions that are not yet tangible. Certainly, do not add any extra code to the system to satisfy these useless tests.

If you simply follow the current requirements, your test code will be easier to read and understand than code that is over-engineered. Prevent confusion for yourself and your teammates.

Big improvements

It's true that writing test code can be hard. However, remember to treat your test code just like your production code. First, validate that your tests will actually fail when they are supposed to. Second, follow the "don't repeat yourself' principle so that maintenance can be easy when the inevitable time arrives. Third, keep your functions tiny. Finally, code your tests to meet only the current requirements.

With these four rules, applied at regular intervals, you will notice a dramatic improvement in the readability, stability, and maintainability of your test automation code.