Sunday, November 4, 2012

Setting up unit tests in a Vala project using CMake

I've recently came across the issue of setting up unit tests in Vala.
Since I haven't found a lot of comprehensive documentation on the web to setup it, I share here my experience so it can be useful to somebody else (I hope).



Setting up a Vala project with CMake

The very first step is to setup the project.
I use the following directory structure:
 +-- AUTHORS
 +-- ChangeLog
 +-- cmake

            |
           +-- vala
                    |
                   +-- FindVala.cmake
                   +-- UseVala.cmake
 +-- CMakeLists.txt 

 +-- COPYING
 +--MAINTAINERS
 +--README
 +-- src

        |
       +-- CMakeLists.txt 
       +-- myclass.vala
 +--tests

        |
       +-- CMakeLists.txt
       +-- testcase.vala
       +-- test-main.vala
       +-- test-myclass.vala

The Vala extensions for CMake

In order to support Vala, you have to add the two files FindVala.cmake and UseVala.cmake in your source tree. They both come from the Vala_CMake
github project from Jakob Westhoff.

$git clone https://github.com/jakobwesthoff/Vala_CMake
So you can also simply add an external reference in your SCM to pull the latest version.

The top level CMakeLists.txt file

Now it is time to add some configuration in our top level CMakeLists.txt file.
The bare minimum for our project is:

cmake_minimum_required (VERSION 2.8)
project (myproject)
enable_testing(true)
 

list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/vala)

find_package(Vala "0.18.0" REQUIRED)
include(${VALA_USE_FILE})


add_subdirectory (src)
add_subdirectory (tests)

 As you can see, three lines are important for Vala support to be enabled.
  • list() tells cmake to load FindVala.cmake and UseVala.cmake
  • find_package() tells cmake to check that vala in version 0.18.0 at least is available
  • include() adds the required includes for compilation.
The enabled_testing() statement is necessary to tell cmake that we want to make unit tests on this project.

The src CMakeLists.txt file

The CMakeLists.txt file located in the src subdirectory contains the necessary for Vala to compile the final binary properly.
The file looks like this:

find_package(PkgConfig)

pkg_check_modules(GLIB REQUIRED glib-2.0)
add_definitions(${GLIB_CFLAGS} ${GLIB_CFLAGS_OTHER})
link_libraries(${GLIB_LIBRARIES})
link_directories(${GLIB_LIBRARY_DIRS})

vala_precompile(VALA_C
  myclass.vala
PACKAGES
    glib-2.0
    posix
GENERATE_VAPI
    mybinary
GENERATE_HEADER
    mybinary
)

add_executable(mybinary ${VALA_C})
add_library(mybinary-clib ${VALA_C})


In this file, you use pkg_check_modules() for any required package that we can find later in the PACKAGES section of the vala_precompile() statement.

If you use GIO, GEE or any other, simply replace GLIB_ by GIO_ or GEE_.

Next you have to precompile your Vala sources in C sources.
In order to do it, you use vala_precompile().
In this example, I asked to generate a .vapi file and two headers, a .h and an _internal.h.
You can also use:
  • CUSTOM_VAPIS and provide a list of the existing .vapi files to use during the precompilation
  • OPTIONS to pass options to the valac executable. Type valac --help to see available options

At this point, you can compile and run your program by typing:
$cmake . && make
$./src/mybinary

The tests CMakeLists.txt file

Now you can configure the CMakeLists.txt file which will compile the unittests program.

enable_testing(true)
set(LIBS ${LIBS} vignette-photo-thumbnailer-clib) 
include_directories(. ../src)

find_package(PkgConfig)
pkg_check_modules(GLIB REQUIRED glib-2.0)
add_definitions(${GLIB_CFLAGS} ${GLIB_CFLAGS_OTHER})
link_libraries(${GLIB_LIBRARIES})
link_directories(${GLIB_LIBRARY_DIRS})

vala_precompile(VALA_C

  testcase.vala
  test-main.vala
  test-myclass.vala
PACKAGES
    glib-2.0
    posix
)

add_executable(unittests ${VALA_C})
target_link_libraries(unittests ${LIBS})
add_test(unittests ${CMAKE_CURRENT_BINARY_DIR}/unittests)


As you can see, the vala_precompile() statement is still here. This is because your tests are written in Vala.

The important things here are:
  • enable_testing(true) which is also present in the top level CMakeLists.txt
  • set() which tell cmake to link your test binary with the static library version of the program to test
  • target_link_libraries() links the static library mybinary-clib.a of our main program that we have generated in the src/CMakeLists.txt file
  • add_test() makes cmake (and thus ctest) aware that the unittests binary is for testing purpose

Now you can run your tests by typing the following command:
$cmake . && make
$ctest 

Writing tests


To write tests, Vala can take advantage of the GLib which provides the GLib.Test namespace.

When using the GLib.Test namespace, you register your test suites in a main.
In our example, this is the test-main.vala file.

using GLib;

public static int main(string[] args)
{
    Test.init(ref args);   
    TestSuite.get_root().add_suite(new TestMyClass().get_suite());
    return Test.run();
}

  
Each suite is provided in a class. The test class contains the usual test cases along with the common set_up() and tear_down() methods.

using GLib;

public class TestMyClass : MyProject.TestCase
{

  public TestMyClass()
  {
    // assign a name for this class
    base("TestMyClass");
    // add test methods
    add_test("test_fail", test_fail);
  }

  public override void set_up()
  {
   // setup your test
  }

  public void test_fail()
  {
   // add your expressions
    assert(1 == 2);
  }

  public override void tear_down()
  {
   // tear down your test
  }
}

You might be wondering what is that inherited MyProject.TestCase class. 
The GLib.Test namespace provides function calls to create a test case and a test suite which is not convenient when manipulating classes.
The guys of the libgee provide a handful wrapper to let you simply inherits from a Gee.TestCase class.
Since I had some issues using this wrapper (maybe because my version of libgee is too old) and that the code is not that complicated, I simply copied and pasted it into my project.

This is the testcase.vala file.

public abstract class MyProject.TestCase : Object
{
    private GLib.TestSuite suite;
    private Adaptor[] adaptors = new Adaptor[0];

    public delegate void TestMethod();

    public TestCase(string name)
    {
        this.suite = new GLib.TestSuite(name);
    }

    public void add_test(string name, owned TestMethod test)
    {
        var adaptor = new Adaptor(name, (owned)test, this);
        this.adaptors += adaptor;

        this.suite.add(new GLib.TestCase(adaptor.name,
                                         adaptor.set_up,
                                         adaptor.run,
                                         adaptor.tear_down));
    }

    public virtual void set_up()
    {
    }

    public virtual void tear_down()
    {
    }

    public GLib.TestSuite get_suite()
    {
        return this.suite;
    }

    private class Adaptor
    {

        public string name { get; private set; }
        private TestMethod test;
        private TestCase test_case;

        public Adaptor(string name,
                       owned TestMethod test,
                       TestCase test_case)
        {
            this.name = name;
            this.test = (owned)test;
            this.test_case = test_case;
        }

        public void set_up(void *fixture)
        {
            this.test_case.set_up();
        }

        public void run(void *fixture)
        {
            this.test();
        }

        public void tear_down(void *fixture)
        {
            this.test_case.tear_down();
        }
    }
}


Now you can run your tests!