Testing music21

Music21 works only because the code has been extensively tested so that new changes are confirmed not to break existing code. Time spent testing always slows down coding in the short run, but long experience has shown that it saves coding time in the long run. Contributions to music21 without tests will always be rejected, so as developers, it’s important that code be well tested.

All functions/methods/etc. need documentation and at least one test. Oh, and that test must pass.

Module-level testing

Every music21 module should have exactly one class called Test which should run the tests for the module. (If the module is getting very big, it is possible to have this class reside in a different module, so long as it is imported into the module that it tests).

Make the Test class be a subclass of unittest and each of the test methods within it must begin with test, for instance testTupletsEndProperly.

import unittest

class Test(unittest.TestCase):

    def testNotesHaveStemDirection(self):
        from music21 import note
        n = note.Note('C#')
        self.assertTrue(hasattr(n, 'stemDirection'))
        n.stemDirection = 'down'
        self.assertEqual(n.stemDirection, 'down')

The Test class must not produce any output if the tests succeed. They must not create new windows, lose the focus from the current window, play music, etc. They must not rely on modules that are not part of the music21 ecosystem (i.e., scipy).

If you need to create a second test that generates external output, create a second unittest.TestCase called TestExternal. Tests should run quickly: what “quickly” means depends on the complexity of what is being contributed; a major new module can add a few seconds to the test suite. A tiny addition, should add milliseconds at most. If your test adds 10 seconds to running, that’s 10 seconds that everyone will have to wait when running test. If you need to have a test that takes a lot of time, create a second Test class called TestSlow.

End your file so that if the module is run directly, it calls these tests.

if __name__ == "__main__":
    import music21
    music21.mainTest(Test)
 .
 ----------------------------------------------------------------------
 Ran 1 test in 0.001s

 OK

instead of music21.mainTest(Test) you can write music21.mainTest(Test, verbose=True) or while debugging music21.mainTest(Test, runTest='testNotesHaveStemDirection') to run a single test. Or to run a single test, you can pass it from the command line:

python3 note.py testNotesHaveStemDirection

You can also call the tests for the module with:

python3 -m music21.note

Documentation Testing

One great feature Python has is the ability to write tests within the documentation itself (“doctest”). We use a lot of these to demonstrate expected behavior in calling a method or function and also to test that the expected behavior works. However, doctests do take up space in the documentation, so do not write anything that can’t be read (use the module-level testing instead).

Doc tests are written like so:

def flipStemDirection(n):
    '''
    Takes in a note with a `.stemDirection` of 'up' or 'down'
    and transforms it to the other.  Does nothing if the `stemDirection` is not defined.

    Notes without stem direction of `up` or `down` are left alone:

    >>> n = note.Note()
    >>> n.stemDirection
    'unspecified'
    >>> flipStemDirection(n)
    >>> n.stemDirection
    'unspecified'

    Here we flip an `up` to `down` and back again.

    >>> n.stemDirection = 'up'
    >>> flipStemDirection(n)
    >>> n.stemDirection
    'down'
    >>> flipStemDirection(n)
    >>> n.stemDirection
    'up'

    Objects without `.stemDirection` raise a Music21Exception

    >>> r = note.Rest()
    >>> flipStemDirection(r)
    Traceback (most recent call last):
    music21.exceptions21.Music21Exception: flipStemDirection only works on Notes
    '''
    try:
        sd = n.stemDirection
    except AttributeError:
        raise exceptions21.Music21Exception("flipStemDirection only works on Notes")

    if sd == 'up':
        n.stemDirection = 'down'
    elif sd == 'down':
        n.stemDirection = 'up'

Generally in a doctest, the full path to the function (assuming from music21 import *) must be given, so if this function were in a hypothetical stem.py file, it would need to be called as stem.flipStemDirection. The reason for this is that we’re writing tests and docs at the same time, so we want to document usage as our users (who are suggested to run from music21 import *) will need to run the routine.

All doctests start with a clean slate of variables, so variables created in a prior doctest will not be available in another.

Running all tests

Once you’ve made sure your contributions to a module work, you’ll want to run all the tests in the system.

The simplest way of running all the tests for music21 is to run:

python3 test/multiprocessTest.py

This will use (n - 1) of your n-processor system to run all of the tests and will print a lot of output along the way. The tests run in reverse-alphabetical order (with some slow tests boosted to the beginning). All failed tests will appear near the bottom of the output.

A few modules are not tested in multiprocessTest because they do not play well with multiprocessing or are in obscure subdirectories. They do not need to be tested on a general basis, but before a major release they should be tested with:

python3 test/singleCoreAll.py

Before a release, this should be run with every version of Python that is supported (and preferably on both Mac and PC). This can be a pain, so fortunately we have…

Continuous Integration Tests

Upon any commit or pull request, continuous integration testing at Travis-CI runs. See:

https://travis-ci.org/cuthbertLab/music21

and hopefully it’s running Green at least for the master branch. Travis-CI runs the travisMain() function of test/singleCoreAll which is the same as the main() function except it exits with 0 or -1 or whatever the UNIX convention for failure is (I always forget–that’s why I coded it). Thanks to the great folks at Travis-CI, the risks of accepting pull requests is lower (though not zero, especially in cases where there are changes to external output or to parsing files outside the corpus). Although the amount of time to run varies depending on the load of travis-ci, keep a look out for changes that significantly slow down the system – they may indicate an inefficient algorithm.

All pull requests must pass Travis-CI to be accepted. It doesn’t matter that “that bug was probably there before” – it’s time to fix it or to get help fixing it.

Code Coverage Tests

One of the systems running on Travis will always run slower than the rest. This system is running coverage to measure how much of the music21 code is being run. For the results see:

https://coveralls.io/github/cuthbertLab/music21

While 100% code coverage is impossible, increasing the coverage percentage with each new contribution is essential. We’re at 90% code coverage and get the green badge of pride on our README.md file, and we like that!

If something should be impossible to trigger, but the code is being maintained for some reason, you can add “# pragma: no cover” to the line or to the fork, but this is generally a no-no.

Testing the User’s Guide

Most new contributions will not change the documentation, but occasionally it will. To make sure that the documentation is correct, run testDocumentation.py (under Python 3 only) in the documentation directory (outside the music21 source code directory). If there are any changes, check to make sure they make sense (maybe there are some new files in the corpus that is changing the count somewhere?) and if they do, rerun the notebook with that documentation (line by line!) and make sure that the new output makes better sense than the previous.

This can take some time, so it does not need to be done for every commit.

Conclusion

The music21 project relies on great tests, both to ensure everything is working properly and also to save time for those who will support your code in the future. One of the main things we can always use contributors to is writing tests for code that is under tested, so please feel free to grab some code from Coveralls and contribute tests for it. It may sound like a small contribution, but it’s one that will be greatly appreciated.