.. _testing: Adding a new test ================= Tests allow us to maintain code quality, ensure that code is working, prevent regressions, and facilitate refactoring. Personally, I find that there is no better motivation for writing tests than knowing that that bug I fixed will stay fixed, and that feature I implemented will not be broken by others. As an open source developer, I never have enough time; tests ensure that what time I have is not wasted. We can divide the existing tests into three classes, based on how they test the Open Babel codebase: 1. Tests written in C++ that test the public API 2. Tests written in Python that use the SWIG bindings to test the public API 3. Tests written in Python that use the command-line executables for testing Which type of test should you write? It doesn't really matter - it's more important that you write *some* type of test. Personally, I can more quickly test more if I write the test in Python, so generally I write and check-in tests of type (2) above; when I need to run a testcase in a debugger, I write a short test of type (1) so that I can step through and see what's happening. Running tests ------------- To begin with, we need to configure CMake to enable tests: ``-DENABLE_TESTS=ON``. This adds the ``make test`` target and builds the C++ tests. For tests of type 3 (above), you will also need to enable the Python bindings: ``-DPYTHON_BINDINGS=ON -DRUN_SWIG=ON``. Some tests are dependent on optional dependencies; if you don't build with support for these, then the corresponding tests will not be run. To actually run the tests, you can run the entire test suite in one go or run individual tests. To run the entire suite, use ``make test`` or ``ctest`` (note that you can use the ``-j`` option to speed up ctest). The ctest command also allows a single test or a list of tests to be specified, and in combination with ``-VV`` (verbose) may be useful to run an individual test. However, I find it more useful to run individual tests directly. Here is an example of how to run an individual test for each of the three types discussed earlier: 1. ``test_runner regressionstest 1`` This will run test number 1 in :file:`regressionstest.cpp`. Nothing will happen...unless the test fails. (test_runner is a testing harness generated by CMake.) 2. ``python test\testbindings.py TestSuite.testAsterisk`` This will run the testAsterisk test in :file:`testbindings.py`. This will write out a single dot, and some summary information. 3. ``python test\testbabel.py testOBabel.testSMItoInChI`` This will run the testSMItoInChI test in :file:`testbabel.py`. The next few sections describe adding a new test of types 1 to 3. The same test will be added, a test to ensure that the molecular weight of ethanol is reported as 46.07. Test using C++ -------------- The easiest place to add new tests is into :file:`test/regressionstest.cpp`. Look at the switch statement at the end of the file and pick a number for the test. Let's say 260. Add the following:: case 260: test_Ethanol_MolWt(); break; Now add the value of 260 to :file:`test/CMakeLists.txt` so that it will be run as part of the testsuite.:: set (regressions_parts 1 221 222 223 224 225 226 227 260) Now let's add the actual test somewhere near the top of the file:: void test_Ethanol_MolWt() { OBMol mol; OBConversion conv; OB_REQUIRE(conv.SetInFormat("smi")); conv.ReadString(&mol, "CCO"); double molwt = mol.GetMolWt(); OB_ASSERT(molwt - 46.07 < 0.01); } The various assert methods are listed in :file:`obtest.h` and are as follows: * **OB_REQUIRE(exp)** - This must evaluate to ``true`` or else the test will be marked as failing and will terminate. This is useful for conditions that *must* be true or else the remaining tests cannot be run (e.g. was the necessary OBFormat found?). * **OB_ASSERT(exp)** - This must evaluate to ``true`` or else the test will be marked as failing. In contrast to OB_REQUIRE, the test does not terminate in this case, but continues to run. This feature can be useful because it lets you know (based on the output) how many and which OB_ASSERT statements failed. * **OB_COMPARE(expA, expB)** - Expressions A and B must be equal or else the test fails (but does not terminate). It is often useful to write a test that uses a checked-in testfile. Let's do this for our example testcase. If you place a file :file:`ethanol.smi` into :file:`test/files`, then the following will read it using a convenience function provided by :file:`obtest.h`.:: void test_Ethanol_MolWt() { OBMolPtr mol = OBTestUtil::ReadFile("ethanol.smi") double molwt = mol.GetMolWt(); OB_ASSERT(molwt - 46.07 < 0.01); } As well as ``ReadFile`` (which is convenient for single molecules), the ``OBTestUtil`` struct provides ``GetFilename`` which will return the full path to the testfile, if you wish to open it yourself. Test using a command-line executable ------------------------------------ At the command-line we can calculate the molecular weight of ethanol as shown below. We are going to do something similar using the Python test framework:: > obabel -:CCO --append MW -otxt 46.0684 Open :file:`test/testbabel.py` in an editor. I have grouped tests related to the :file:`obabel` executable into a class testOBabel, so let's add a new test there. Somewhere in that class (for example, at the end), add a function such as the following (note: it must begin with the word "test"):: def testMolWtEthanol(self): """Check the molecular weight of ethanol""" self.canFindExecutable("obabel") answers = [ ("CCO", 46.07), ("[H]", 1.01), ("[2H]", 2.01), ] for smi, molwt in answers: output, error = run_exec('obabel -:%s --append mw -otxt' % smi) my_molwt = round(float(output), 2) self.assertEqual(my_molwt, molwt) We provide a few convenience functions to help write these tests. The most important of these is ``run_exec(command)`` which runs the commandline executable returns a tuple of stdout and stderr. Behind the scenes, it adds the full path to the named executable. In the example above, ``run_exec(stdin, command)`` took a single argument; the next example will show its use with two arguments - the additional argument is a string which is treated as stdin, and piped through the executable. In the previous example, each SMILES string was passed in one-at-a-time. However, it is more efficient to do them all in one go as in the following example:: def testMolWtEthanol(self): """Check the molecular weight of ethanol""" self.canFindExecutable("obabel") smifile = """CCO [H] [2H] """ answers = [46.07, 1.01, 2.01] output, error = run_exec(smifile, 'obabel -ismi --append mw -otxt') for ans, my_ans in zip(answers, output.split("\n")): self.assertEqual(ans, round(float(my_ans), 2)) To use a testfile placed in :file:`test/files`, the ``getTestFile()`` member function is provided:: def testMolWtEthanol(self): """Check the molecular weight of ethanol""" self.canFindExecutable("obabel") answers = [46.07, 1.01, 2.01] smifile = self.getTestFile("ethanol.smi") output, error = run_exec('obabel %s --append mw -otxt' % smifile) for ans, my_ans in zip(answers, output.split("\n")): self.assertEqual(ans, round(float(my_ans), 2)) The full list of provided convenience functions is: * **run_exec(command)**, **run_exec(stdin, command)** - see above * **BaseTest.getTestFile(filename)** - returns the full path to a testfile * **BaseTest.canFindExecutable(executable)** - checks whether the executable exists in the expected location * **BaseTest.assertConverted(stderr, N)** - An assert statement that takes the stderr from run_exec and will check whether the number of molecules reported as converted matches N Test the API using Python ------------------------- The easiest place to add new tests is into :file:`test/testbindings.py`. Classes are used to organise the tests, but for a single 'miscellaneous' test a good place is the TestSuite class. Somewhere in that class add the following function:: def testMolWtEthanol(self): """Check the molecular weight of ethanol""" answers = [ ("CCO", 46.07), ("[H]", 1.01), ("[2H]", 2.01), ] for smi, molwt in answers: my_molwt = round(pybel.readstring("smi", smi).molwt, 2) self.assertEqual(my_molwt, molwt) The variable ``here`` is defined in :file:`testbindings.py` and may be used find the path to testfiles. For example, given the :file:`test/ethanol.smi`, the following may be used to read it:: def testMolWtEthanol(self): """Check the molecular weight of ethanol""" answers = [46.07, 1.01, 2.01] testfile = os.path.join(here, "test", "ethanol.smi") for mol, answer in zip(pybel.readfile("smi", testfile), answers): my_molwt = round(mol.molwt, 2) self.assertEqual(my_molwt, molwt) The tests use the standard unittest framework. One thing to note, which is not obvious, is how to test for exceptions. A typical case is checking that a dodgy SMILES is rejected on reading; in this instance, ``Pybel.readstring()`` will raise an IOError. To assert that this is the case, rather than use try/except, the following syntax is required:: self.assertRaises(IOError, pybel.readstring, "smi", "~*&*($") If you have multiple tests to add on a single 'topic', you will probably want to add your own class either into :file:`testbindings.py` or a new Python file. Note that if you create a new Python file, it should start with the word ``test`` and you will need to add the rest of the name to the ``pybindtest`` list in :file:`test/CMakeLists.txt`. Some final comments ------------------- Some thoughts on the topic of the perfect test: * When adding a regression test for a bug fix, the test should fail without the fix, but pass afterwards. * When adding a test for a new feature, the test should provide complete coverage for all code paths. * Test not just perfect input, but all sorts of dodgy input like molecules with no atoms, empty strings, and so forth. * Don't be afraid to add tests for things which you already (think you) know will pass; such tests may surprise you, and even if they don't they will prevent regressions. Potential problems/gotchas: * Why isn't your Python test being run? Test functions name must begin with the word ``test``. * If your new test passes first time, check that it is actually running correctly, by changing your asserts so that they should fail. * The C++ tests will be marked as failing if the test writes any of the following to stdout: ``ERROR``, ``FAIL``, ``Test failed``. This is actually how the assert methods work. * It's best to avoid writing to disk, and instead write to a variable or stdout and capture it (as in the examples above).