1. Introduction
Testing is an important part of your applications, so Crails comes with tools to help you run all sorts of tests.
2. Writing tests
2.1 Adding a new test
The crails CLI tool comes with a scaffold for tests. Run the following command:
$ crails scaffold test --name MySpec
This will generate a new MySpec
class. Open up the source file:
spec/my_spec.cpp
#include "my_spec.hpp"
MySpec::MySpec()
{
before(std::bind(&MySpec::before_all, this));
after(std::bind(&MySpec::after_all, this));
describe("name of a method", [this]()
{
it("fails", [this]()
{
EXPECT(1, ==, 2);
});
});
}
void MySpec::before_all()
{
}
void MySpec::after_all()
{
}
Tests are reparted in categories defined with the describe
method. Then,
each test unit is defined using the it
method, typically followed with
a short sentence describing the expectation of the test, such as:
it("should succeed", callback)
.
The EXPECT
macro is used to define the success conditions of a test. A test
may contain any number of call to EXPECT
, or none at all: another way to fail
a test can be to throw an exception.
Next step. we need to register the test in the runner:
spec/spec.cpp
#include <crails/tests/runner.hpp>
#include "my_spec.hpp"
using namespace std;
void Crails::Tests::Runner::setup()
{
helpers.push_back(make_shared<MySpec>());
}
void Crails::Tests::Runner::shutdown()
{
}
Our test is ready to fail with success. Run the following command:
$ crails build $ build/tests
2.2 Request tests
Crails provide a simple way to write request tests, in which you simulate an HTTP query and analyze its response. Let's update our previous test to run a test request:
spec/my_spec.hpp
#include <crails/tests/helper.hpp>
#include <crails/tests/request.hpp>
class MySpec : public Crails::Tests::Helper
{
MySpec()
{
using namespace Crails;
describe("index", []()
{
it("lists items", []()
{
Tests::Request request(HttpVerb::get, "/route");
request.run();
EXPECT_STATUS(request, HttpStatus::not_found);
});
});
}
};
If you run the test right now, you'll see an exception thrown, warning you
that the Router
is not initialized. The router needs to be
initialized and finalized as the test suite runs and stops:
spec/spec.cpp
#include <crails/tests/runner.hpp>
#include <crails/router.hpp>
#include "my_spec.hpp"
using namespace std;
void Crails::Tests::Runner::setup()
{
Crails::Router::singleton::initialize();
Crails::Router::singleton::get()->initialize();
helpers.push_back(make_shared<MySpec>());
}
void Crails::Tests::Runner::shutdown()
{
Crails::Router::singleton::finalize();
}
Our test should now pass: build and run the test suite:
$ crails build $ build/tests
2.2.1 Injecting params in a request
Instead of building fully-fledged http requests for your tests, you may want to bypass that step and directly inject values into the context's params object.
In the following exemple, we will use Test::Request
's params
attribute to inject the parameters of a search query:
spec/my_spec
#include <crails/tests/helper.hpp>
#include <crails/tests/request.hpp>
class MySpec : public Crails::Tests::Helper
{
MySpec()
{
using namespace Crails;
describe("index", []()
{
it("lists items", []()
{
Tests::Request request(HttpVerb::post, "/search-song");
DataTree response;
request.params["title"] = "Conscious Club";
request.params["year"] = 2016;
request.run();
EXPECT_STATUS(request, HttpStatus::ok);
response.from_json(request.response.body());
EXPECT(response["artist"].as<string>(), ==, "Vulfpeck");
EXPECT(response["album"].as<string>(), ==, "Conscious Club");
EXPECT(response["track"].as<int>(), ==, 4);
});
});
}
};
2.2.2 Session parameters
You may also inject your own session variables into a request in a similar fashion:
spec/my_spec.cpp
#include <crails/tests/helper.hpp>
#include <crails/tests/request.hpp>
class MySpec : public Crails::Tests::Helper
{
MySpec()
{
using namespace Crails;
describe("index", []()
{
it("lists items", []()
{
Tests::Request request(HttpVerb::get, "/private");
request.session["current_user_id"] = 1;
request.run();
EXPECT_STATUS(request, HttpStatus::ok);
});
});
}
};
With session variables though, you may also be interested in the state of the session after the request completed. For instance, to check if an user has properly been disconnected:
spec/my_spec.cpp
#include <crails/tests/helper.hpp>
#include <crails/tests/request.hpp>
class MySpec : public Crails::Tests::Helper
{
MySpec()
{
using namespace Crails;
describe("index", []()
{
it("lists items", []()
{
Tests::Request request(HttpVerb::post, "/disconnect");
request.session["current_user_id"] = 1;
request.run();
EXPECT_STATUS(request, HttpStatus::ok);
EXPECT(request.session["current_session_id"].exists(), !=, true);
});
});
}
};
2.3 Test preparation
You might want to run code before and after each tests. The after
and before
methods cam help you define such behaviors for each test group:
spec/my_spec.hpp
#include <crails/tests/helper.hpp>
#include <crails/tests/request.hpp>
class MySpec : public Crails::Tests::Helper
{
MySpec()
{
using namespace Crails;
before([this]()
{
std::cout << "before each tests" << std::endl;
});
after([this]()
{
std::cout << "after each tests" << std::endl;
});
describe("index", [this]()
{
before([this]()
{
std::cout << "before each index tests" << std::endl;
});
after([this]()
{
std::cout << "after each index tests" << std::endl;
});
it("lists items", [this]()
{
Tests::Request request(HttpVerb::get, "/route");
request.run();
EXPECT_STATUS(request, HttpStatus::not_found);
});
});
}
};
3. End-to-end testing
You may want to go deeper, and write tests that inspect the
web pages rendered by your application. To perform end-to-end
testing, we use the libcrails-selenium
module.
Install the selenium plugin with the following command:
$ crails plugin selenium install
This command will download the selenium driver, and include new helpers for your tests.
3.1 Configure a background server
For end-to-end testing to work, we need a server to run in the background, available for our headless browser to interact with it.
Update the spec.cpp
file to use the Crails::Tests::BackgroundServer
singleton:
spec/spec.cpp
#include <crails/tests/runner.hpp>
#include "my_spec.hpp"
#include <crails/tests/background_server.hpp>
using namespace std;
void Crails::Tests::Runner::setup()
{
Crails::Tests::BackgroundServer::initialize();
helpers.push_back(make_shared<MySpec>());
}
void Crails::Tests::Runner::shutdown()
{
Crails::Tests::BackgroundServer::finalize();
}
3.2 Writing an end-to-end test
End-to-end test helpers should inherit from Crails::Tests::SeleniumHelper
. A browser will
be launched each time such a helper is triggered, and you will have access to new methods within the
helper to interact with the browser. Before we take a look at that, update our spec header to include
Crails::Tests::SeleniumHelper
:
spec/my_spec.hpp
#pragma once
#include <crails/tests/helper.hpp>
#include <crails/tests/selenium_helper.hpp>
class MySpec : public Crails::Tests::SeleniumHelper
{
public:
MySpec();
private:
void before_all();
void after_all();
};
We're now ready to update our test so that it tests a page in the browser:
spec/my_spec.cpp
#include "my_spec.hpp"
MySpec::MySpec()
{
before(std::bind(&MySpec::before_all, this));
after(std::bind(&MySpec::after_all, this));
describe("name of a method", [this]()
{
it("fails", [this]()
{
page->visit("/");
page->expect()
.to_have_element("body > h1")
.with_content("Welcome to your new Crails Application");
});
});
}
void MySpec::before_all()
{
}
void MySpec::after_all()
{
}
As you can see on this example, you have no access to a page
property, which allows you to
interact with the test browser loaded by Selenium. We use the page->visit
method to
load a given URI to the browser.
The page
controller also gives you access to a new expectation system. You can still use
the EXPECT
macro, but selenium based expectation will allow you to navigate within the DOM
of your generated page, check for the presence of elements, their contents, and such.
3.3 Interacting with page elements
The libcrails-selenium
plugin leverages the webdriverxx
library: its README will tell you all you need to know about it.
To setup your test, you'll need to interact with page elements. You can get a handle on elements
using the page->find
method, using a selector string:
webdriverxx::Element element = page->find("input[type=\"text\"]");
element.SendKeys("Hello, world !");
page->find("input[type\"submit\"]").Click();