Testing

From GnuCash
Revision as of 23:38, 23 March 2017 by Jralls (talk | contribs) (Travis CI: fix typo)
Jump to: navigation, search

A good set of tests is a critical requirement for modern software development, both to ensure the quality of the product and to help developers to quickly modify code without introducing bugs or causing regressions. The literature on software testing is vast; excellent tutorials and references are available both online and in print, see Literature Survey.

All developers are encouraged to review tests and ensure that any work is covered by both acceptance and unit tests.

Current Test Architecture

Acceptance Tests

Acceptance tests in Gnucash are based on a home-grown (or anonymously sourced) set of macros and functions which can be found in src/test-core and src/engine/test-core. The quality, scope, and coverage of these tests varies greatly; some parts of GnuCash are tested lightly or not at all while others are tested fairly extensively. Although I've labelled them "acceptance tests", in many cases they're written more as unit tests whose scope is a single function rather than a whole module.

Unit Tests

Unit testing, invented by Kent Beck in the early 1990s, seeks to test the public interface of classes as thoroughly as possible: All member functions should be tested with as much variation of their parameters as possible, with an emphasis on corner cases. Tests should avoid dependence on the implementation to avoid brittleness.

GnuCash has adopted the GLib testing framework to facilitate unit testing of GObject-based classes and GLib-dependent code. Muslim Choclov wrote unit tests for the most important modules in LibQOF as a GSoC2011 project. Work continues to get all of LibQOF and the engine fully tested to facilitate major architectural changes needed to make GnuCash a proper database application.

For C++ code GnuCash has adopted the Google Test Framework.

See also https://developer.gnome.org/glib/stable/gtester.html for the actual command line tool to run tests.

Running Tests

The collection of all implemented tests in gnucash is run by running

make check

This can be run in any subdirectory to limit the number of tests. If a directory has both acceptance and unit tests, one can further limit the testing to the unit tests by running

 make test

which you'd run also if you're working on unit tests and need to recompile. Note that make test doesn't always work outside of the test directory itself. You can also run the unit tests in a directory with the test program, and restrict the tests run with the appropriate suite-name or even test case:

test-engine -p /engine/Account

will run only the Account tests. The test program is really an alias for gtester; read the man page for more details and options, e.g. the --verbose argument.

Travis CI

We have set up an automatic build and test bot on Travis CI to run the full test suite after every commit and for every Github pull request. The Travis build and test environment is configured through the .travis.yml file in the GnuCash root directory. Individuals with Github forks can easily arrange to run Travis on commits to their repositories as well from the Github repository's Settings Tab: Select Integrations & Services then select Travis CI from the drop down list, enter your password, and fill out the form.

Travis keeps track of the status of each branch's last test and will email the pusher if a push fails to build or test or if the previous push failed and the latest one didn't. Obviously if Travis reports that the branch is fixed no action is required. If Travis reports a failure, follow the link in the email and examine the output. If it indicates a problem with the commit then you should push a fix, but sometimes it will indicate an unrelated error. There are some tests that fail at random; you can look at the build history tab on the Travis page to see old failures and to compare the failing tests to see which tests they are. In those cases rerunning the build on Travis (the button is at the top left of the build page) will usually produce a passing test and a happy Travis.

For tests that fail on Travis but not on your local development system it can be helpful to keep a VM around that duplicates the Travis build environment, which you can determine by examining .travis.yml. Note that the default Travis environment is Ubuntu Precise (12.04), which maint uses; master changes that to Ubuntu Trusty (14.04) via the .travis.yml

 dist: trusty

Policy

  • All new non-GUI code should include thorough unit tests. Automated testing of the GUI tends to be brittle, so GUI modifications should be hand tested in as many OS environments as possible before being committed.
  • make check run from the top build directory should pass before commits are pushed.

Unit Test Policies

  • Getter/Setter functions which only set or retrieve an instance member variable do not need to be tested.
  • Convenience functions which only wrap another function to change the function's name or to provide a default argument do not require testing.
  • Composed functions, or functions which simply string together a series of calls to other functions, need not be tested if the called functions are all tested, have no side effects, and where the composed function has only one flow of control.
  • There is some disagreement among testing gurus about whether a function's parameter variations should be exercised in a single test function or separately in a test function per function call. Use your judgement here. Remember that the dictum of Agile Development is to write a little bit at a time and to refactor as often as you need to. That applies as much to test code as it does to production code. It's OK to change your mind!
  • Similarly there is tension among the gurus about how much to make a test program dependent upon, and how much to use mock objects to replace actual dependency code. Keeping in mind the goal of a short code-compile-test cycle, use your judgement. That said, at present much of Gnucash is rather interdependent and doesn't virtualize functions -- a requirement for applying mocks. If you're writing new modules, do use modern OO techniques to minimize interdependence, and where it's necessary make sure to use virtual functions so that linking the rest of Gnucash isn't needed to test your work.


Writing Tests

Legacy Tests

GLib-test based tests

Read the GLib test documentation. To set up unit testing in a directory:

  • Create a "test" directory if there isn't one already
  • Create a Makefile.am in that test directory, again if there isn't one already.
  • Add the line
 include $(top_srcdir)/test-templates/Makefile.decl

in the Makefile.am and in every parent directory's Makefile.am that doesn't already have that line. If a parent directory has more than one subdirectory and not all of them have unit tests, create or add to the variable TEST_SUBDIRS.

  • You will be adding one or more test programs. These are declared by adding their names to the TEST_PROGS variable, then defining the usual automake variables for each. For example:
 TEST_PROGS += test-foo test-bar
 noinst_PROGRAMS = $(TEST_PROGS)
 
 test_foo_SOURCES = bar.c baz.c test-foo.c utest-baz.c
 test_foo_HEADERS = foo.h  
 test_foo_LDADD   = ../libgncmod-pepper.la
 (And so on, also for test_bar)

There is a unit test support module with some useful functions for controlling logging and signals in src/test-core. If you use it, add

 ${top_builddir}/src/test-core/libtest-core.la

to your test program's LDADD variable or add

 ${top_srcdir}/src/test-core/unittest-support.c

to the SOURCES variable and

 #include <unittest-support.h>

in your test-suite file. You have two options for the actual test programs. You can write a bunch of separate programs with a few tests each or you can group several files containing tests into a single program.

Many Little Programs

  • Make a copy of test-templates/testmain.c for each program, renaming it appriopriately, in your test directory.
  • Create fixtures and test functions and register the test functions in main(); there are comments in the file to guide you.
  • Set up a target in Makefile.am as described above for each program.

Test Suites

A test suite is a collection of test functions registered in a test-suite function; main() runs the test-suite functions. This makes it easier to group tests into separate files with a master test program file to contain main(). We'll call the master test program source file the module file; it's conventionally named after the directory it's testing, e.g. test-engine.c. Normally you'll have a test-suite for each source file in the directory named utest-filename.c, e.g. utest-Split.c.

Note that as the C++ conversion progresses it may be necessary to compile some of these files as C++ in which case the extension will be .cpp instead of .c.

  • Copy test-templates/test-module.c to your test directory, rename it, and create a target for it in Makefile.am.
  • Run
 test-templates/make-testfile "Your Name <you@your.email.address>" path/to/source

passing the path to the first source file you want to write tests for. This will create a template test file for you with all of the necessary functions prototyped and commented out and a populated test suite function.

  • Add the file just created to the SOURCES of your test target in Makefile.am.

Google Test based tests

Test Coverage Status

Unit Tests

LibQOF

Done:

  • qofbackend
  • qofbook
  • qofinstance
  • qofobject
  • qofsession
  • gnc-date
  • kvp_frame

Engine

Done:

  • Account
  • Split
  • Transaction

Known Test Needs

  • GtkAction callbacks referenced directly in GtkBuilder UI files need at a minimum "presence testing" so that make check will fail if the callbacks don't compile for some reason.

Literature Survey

  • Test Patterns: Refactoring Test Code The skeleton of this excellent manual for writing and improving unit tests is online; there is a pointer there for purchasing the book as well. While the book focuses on xUnit-style test frameworks (meaning jUnit and its many derivatives) most of the principles and patterns are applicable to any unit test code.