BDT – Automating Your Application Testing in Behavior-Driven Way

 

Author Bio: 
Anastasia Stefanuk is a passionate writer and a marketing manager at Mobilunity. The company provides IT outstaffing services, so she is always aware of technology news and wants to share her experience to help tech startups and companies to be up-to-date.

Every qualified developer, in-house one or a freelancer or the one who works on a basis of IT outstaffing model, the testing process is of a great importance. As software projects become more and more complex, it becomes increasingly difficult to detect bugs that can compromise user experience and negatively affect businesses. To this end, development frameworks and methodologies have been created for the purpose of addressing such issues and hopefully making the development of large applications a less stressful endeavor.

Test-driven development, for instance, is a process of software development wherein developers first have to code tests that the application should pass. The application will initially fail all these tests, but will gradually pass each one of them as the development progresses. This makes it a lot easier for developers to detect bugs in the system, because such bugs will cause the application to fail some of tests.

The effectiveness of test-driven development, however, depends solely on the developers who coded these tests, and these developers can easily miss some requirements from the client. Behavior-driven development takes this a step further by allowing non-technical members of the team, such as business analysts and the management, to establish a deeper involvement in development process. One way this is implemented is through behavior-driven testing, or BDT, in which the test cases are highly abstracted and are written in natural language that can easily be understood, edited, or even written by non-technical members. In this way,  instead of giving strong and formal names for calling methods, we are building the chain of calls which fold into a unified sentence that describes the application’s behavior. Let’s see how this methodology has appeared, how it has been evolving, and how it’s being applied in modern PHP/JavaScript libraries and frameworks.

The Motivation Behind BDT

Descriptive style takes its roots from the «chain» pattern, in which the methods enclosed in classes return some reference on themselves. This allows you to call a few methods in a single statement:

public class SomeClass
{
    public SomeClass someMethod()
    {
        // do some
        return this;
    }

    public SomeClass anotherMethod()
    {
        // do some
        return this;
    }
}

// somewhere
SomeClass chain = new SomeClass();


chain
    .someMethod()
    .anotherMethod();

In PHP, this syntax became widely known and used in the builders of database queries from the Zend Framework library:

$db->select()
    ->from('posts')
    ->where('status = ?', 'publish')
    ->order('updated_at DESC')
    ->limit(0, 20);

This block of code, being a simple chain of calls, previously looked like an SQL statement. In such a way it became more readable and understandable. Unfortunately, the major part of ZF1 classes (in the latest versions as well) follows the classic concept with long methods and property names describing their functionality:

class ErrorController extends Zend_Controller_Action
{
    public function errorAction()
    {
        $errors   = $this->_getParam('error_handler');
        $response = $this->getResponse();

        $response->setHttpResponseCode(500);
        $this->view->message   = 'Application error';
        $this->view->exception = $errors->exception;

        $this->view->request   = $errors->request;
    }
}

As opposed to Zend, modern frameworks like Laravel are entirely based on the chain syntax and descriptive coding style:

class PostsController extends Controller
{
    public function index()
    {
        $posts = app()->make(App\Post::class)
            ->where('status', 'active')
            ->orderBy('updated_at', 'desc')
            ->paginate();

        return view('posts.index')->with([
            'posts' => $posts
        ])
    }
}

In this example we can see that helpers and method names are laconic and not always meaningful like they are in Zend, but they act like parts of a sentence and build a detailed description of the action, which those parts of the code perform. You can easily transform them into English sentences:

«(ask) app (to) make App\Post class, (get posts) where (their) status is active, order (them) by updated_at (attribute) descending (and return) paginate(d list)»

«return view (page) posts.index (or index page of posts section) with posts (list)»

Still, this approach reveals its full potential in describing cases for automated testing. Each test case, which is based on the behavior-driven concept instead of simple assert statements, is understandable for non-programmers too. Therefore, it can be written by QA engineers with the basic level of programming language knowledge, thereby helping the company reduce the cost of development by eliminating the need to hire additional highly skilled developers.

Laravel

We have witnessed the modern coding style from the Laravel example, so let’s start with this framework. Its test suite is based on the popular PHPUnit library, but its base TestCase class adds a lot of chaining methods allowing you to easily write behavior-driven tests. Also, Laravel has a built-in web server for testing purposes, so it’s also possible to write automated tests which simulate a user’s actions. The developer can write a test case which opens a specific page (in silent mode), clicks at links, sends forms and checks how the application responds to it all. The fixture mechanism is present as well, so the database state is restored before each case is run.

Here is an example of testing user login and register functionalities:

class UsersTest extends TestCase
{
    // using special trait which will roll back
    // all changes in tearDown() and restore
    // database to initial state
    use DatabaseTransactions;

    // test user data
    private static $newUserData = [
        'name' => 'newuser',
        'email' => 'newuser@gmail.com',
        'password' => 'newpassword'
    ];

    public function testRegisterUser()
    {
        // fetching user data without password
        // which will be used as search filter
        // (as it's not stored in database)
        $newUser = array_except(static::$newUserData, 'password');

        $this->dontSeeInDatabase('users', $newUser) // (let's) see (that new)
            // user wasn't found in the database
            ->registerUser() // register user
            ->seePageIs('/home') // (let's) see (that after register)
            // page is /home
            ->dontSee('Login') // (let's) see that we don't have
            // Login (link) on the page
            ->dontSee('Register') // (let's) see that we don't have
            // Register (link) on the page
            ->see('Logout') // (let's) see that we have Logout
            // (link) on the page
            ->see($newUser['name']) // (let's) see that we have
            // new user name on the page
            ->seeInDatabase('users', $newUser); (let's) see that we (now)
            // have a new user on the page
    }

    public function testRegisterAndLogin()
    {
        // getting test user data
        $data = static::$newUserData;

        $this->registerUser() // registering user
            ->press('Logout') // logging out
            ->type($data['email'], 'email') // typing test user email
            ->type($data['password'], 'password')  // and password
            ->press('Login') // pressing Login button
            ->seePageIs('/home') // checking is page /home
            ->see($data['name']); // checking is test user name
            // displaying in the navigation area
    }

    protected function registerUser()
    {
        // getting test user data
        $data = static::$newUserData;
        $password = $data['password'];

        return $this->visit('/register') // opening /register page
            ->type($data['name'], 'name') // typing user data
            ->type($data['email'], 'email') // into register form
            ->type($password, 'password')
            ->type($password, 'password_confirmation')
            ->press('Register'); // pressing Register
    }
}

As we can see, all test cases present chains of transparent instructions and descriptions of expected application behavior. If you compare method names in chains and their comments on the right side – you can notice that they are pretty similar.

Chai.JS

A few BDT assertion libraries have been developed for JavaScript testing systems (Mocha.JS, Jasmine, etc), replacing the default assertion methods which are less intuitive and require higher technical knowledge to comprehend. Chai.JS is the most popular among them. It provides except() and should() methods, which allows developers to wrap variables in actual results and build assertion chains in a behavior-describing style.

The differences between default and BDT assert statements can be demonstrated in the following example. Let’s imagine that we have an «adder» class and we need to test its addition() method:

const SimpleMath = require('../src/simple-math');
const chai = require('chai');
const assert = chai.assert;
const expect = chai.expect;

describe('SimpleMath:chai', () => {
    const operand1 = 1;
    const operand2 = 2;
    const expectedResult = 3;
    const result = SimpleMath.addition(operand1, operand2);

    // default assert
    describe('addition:assert', () => {
     it(`${operand1} + ${operand2} should be ${expectedResult}`, () => {
        // check if the type of result is the number
        assert.typeOf(result, 'number');
        // check if the result equals 3
        assert.equal(result, expectedResult);
      });
    });

    // expect style
    describe('addition:expect', () => {
     it(`${operand1} + ${operand2} should be ${expectedResult}`, () => {
        // we are expecting that result will be a number
        expect(result).to.be.a('number');
        // we are expecting the result equal 3
        expect(result).to.equal(expectedResult);
       });
    });

    // should style
    describe('addition:should', () => {
     it(`${operand1} + ${operand2} should be ${expectedResult}`, () => {
        chai.should();
        // result should be a number
        result.should.be.a('number');
        // result should equal 3
        result.should.equal(expectedResult);
       });
    });
});

The expect/should style assert statements are almost identical to their comments, in particular the «should» style — it totally repeats the case description in English.

Chai.JS also provides many other chain tokens to check the most complex datasets, including and-tokens for joining chains. For example, if we are testing user login through some REST API, we need to check some JSON response. It may contain success property (true or false) and also (in case of success) data property with the logged user’s data (i.e. id, name, email). Using expect(), we can write the following response verifications:

expect(json).to.have.property('success')
    .that.is.a('boolean')
    .and.equal(true);

expect(json).to.have.property('data')
    .that.is.an('object')
    .and.that.not.to.be.empty;

expect(json).to.have.property('data')
    .that.contain.all.keys('id', 'name', 'email');

expect(json).to.have.deep.property('data.email')
    .that.is.a('string')
    .and.that.equal(email);

We don’t need any comments on this example, since everything is quite clear without them.

NightWatch.JS

Another JavaScript testing library provides behavior-driven tests with chain code syntax is NightWatch. It’s an automation testing system, that allows testers to write scripts in order to test real web pages in real browsers. To emulate browser actions, it uses a Selenium server or direct browser manipulation libraries (such as ChromeDriver). Besides the chained actions and assertions (which are similar to Laravel’s), NightWatch provides a special feature called «page objects». These are web page skeletons, where developers can specify comprehensible names (like header, footer, navigation, loginForm) for common parts of the UI. Let’s see how it works.

Let’s take the website with two pages as an example: homepage and «About Us». On the homepage we have the header with «Homepage» text and the link to the «About Us» page with the corresponding text. On the «About Us» one, the header contains only the name of the page. Such pages can be represented as the following objects:

// pages/home.js
module.exports = {
  url: "http://site-being-tested.com",
  elements: {
    header: {
      selector: "h1:first-of-type"
    },
    aboutLink: {
      selector: "a[href*=\"about\"]"
    }
  }
};

// pages/about.js
module.exports = {
  url: "http://site-being-tested.com/about",
  elements: {
    header: {
      selector: "h1:first-of-type"
    }
  }
};

All technical information such as page <em>urls, elements, and selectors</em> are encapsulated into those objects. So, in the test cases we are using only understandable method and reference syntax in accordance with the BDT style:

module.exports = {
  "Visit Homepage": (client) => {
    const home = client.page.home(); // getting homepage object

    home.navigate() // opening homepage
      .assert.containsText('@header', 'Homepage'); // checking if the header
                                                 // contains "Homepage"

    client.end(); // closing browser
  },

  "Click on About Us link": (client) => {
    const home = client.page.home(); // getting home/about pages objects
    const about = client.page.about();

    about.navigate() // opening homepage
      .assert.elementPresent('@aboutLink') // checking if it has
                                           // the link to about page
      .click('@aboutLink') // clicking on the link to about page
      .assert.urlEquals(about.url); // checking if we are
                                    // redirected to about page

    client.end(); // closing browser
  },

  "Visit About Us": (client) => {
    const about = client.page.about(); // getting about page object

    about.navigate() // opening about page
      .assert.containsText('@header', 'About Us'); // checking if the header
                                                   // contains "About Us"

    client.end(); // closing browser
  }
};

Conclusion

Business-driven testing or BDT allows non-technical members of any development team a stronger involvement in the development process by allowing them to comprehend test cases and even create their own. Overall, the BDT is a popular testing concept today and it’s implemented through the big number of modern frameworks and libraries. In this article, we’ve seen examples of how BDT is implemented through different frameworks and libraries. The future is in behavior-driving testing, that’s why we recommend developers to get acquainted with it and start implementing this technique in their own projects just like our dedicated development teams are doing.

Join our mailing list:

Comments

  1. says

    I’m a long time reader, who doesn’t know C++ or most of the other compiled languages, so it’s a real treat to see PHP and JavaScript on what’s maybe my favorite tech blog. A great read on a concept that gets more important the deeper into web dev I get.

    One problem though… This isn’t a bunny!

    • "No Bugs" Hare says

      > it’s a real treat to see PHP and JavaScript on what’s maybe my favorite tech blog.
      > One problem though… This isn’t a bunny!

      Well, unfortunately I don’t know JS and PHP _that_ well (I can write in them, but certainly cannot qualify as an expert); also, TBH, for larger projects I tend to prefer strong compile-type typing (it certainly helps to get rid of lots of bugs – even before asserts have to kick in). OTOH, whenever I see good content about JS/PHP, I can recognize it, so if somebody else offers another good guest post on JS/PHP/Python/… – I’ll be happy to post it :-).

  2. says

    WOW. What a knowledgeable post.
    I and pleased and Thankful to you. I have learnt BDT implemented through different frameworks and libraries.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.