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.