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();