Hello all. Auto-testing software is a great thing - it avoids hours of manual testing, allows you to keep all your testing requirements in code, and (most usefully) allows you to easily re-run a full set of tests after you make a small change to your code.
This is how we have been doing testing on a simple experiment which displays images, asks visual search questions and collects keypresses and RTs.
The first step is to make sure that your code can be easily called by the test framework. So we encapsulated the experiment inside a function:
def run_blocks(trials,noise,win,expInfo,incorrect,tone1,tone2,experiment_details,allPoints,n_blocks,trials_per_block):
âŚto which we pass all the necessary details.
The real experiment file, experiment.py, simply imports this function loads up the trial data and audio stimuli and calls this function.
Our testing file, test.py, imports this function too, and then defines a set of tests using the Unittest framework. Each one looks like this:
class reaction_time_zero(unittest.TestCase):
@mock.patch('keyboard.wait_keys')
@mock.patch('keyboard.get_keys')
def runTest(self, mock_get_keys, mock_wait_keys):
print 'TEST: ' + self.__class__.__name__
experiment_details = {}
trials = {}
n = 1
trials[1] = {}
trials[1]['image_name'] = '2008_000177.jpg'
trials[1]['present_absent'] = 'No Target'
trials[1]['trial_type'] = 'Critical'
trials[1]['tone_hz'] = 'tone1'
trials[1]['tone_onset'] = 500
mock_wait_keys.return_value = [['q', 0]]
mock_get_keys.return_value = [['space', 0]]
data = run_blocks(trials,noise,win,expInfo, incorrect, tone1, tone2, experiment_details,allPoints,1,n)
t = data[1]
print t
self.assertEqual(t['RT_TO'], 0)
self.assertEqual(t['tone_sdt'], 'HI')
Each test has three parts: setup, running and checking the results. This test just runs one trial: setup is done by setting the trial details:
trials[1] = {}
trials[1]['image_name'] = '2008_000177.jpg'
trials[1]['present_absent'] = 'No Target'
trials[1]['trial_type'] = 'Critical'
trials[1]['tone_hz'] = 'tone1'
trials[1]['tone_onset'] = 500
Next, we want to simulate keypresses. We do this by locating the function which would wait for keypresses (getKeys or waitKeys) and replacing it with a fake test function which instantly returns the key we specify. This is called mocking.
Now, we could set up a mock for getKeys with @mock.patch(âpsychopy.event.getKeysâ), but it turns out that waitKeys also calls getKeys, so we wouldnât be able to specify different behaviours for waitKeys and getKeys.
We solve this by simply wrapping getKeys in our own function, which in the normal experiment will pass straight through to getKeys:
def get_keys(keyList, timeStamped):
return event.getKeys(keyList=keyList, timeStamped=timeStamped)
we can then mock this using @mock.patch(âkeyboard.get_keysâ).
So we set up two mocks:
@mock.patch('keyboard.wait_keys')
@mock.patch('keyboard.get_keys')
which we then pass into the test function in reverse order (Python decorators, like these mock setups, are evaluated in reverse order):
def runTest(self, mock_get_keys, mock_wait_keys):
Next, we have to specify the behaviour of the mocks. We can specify a constant return value with
mock_wait_keys.return_value = [['q', 0]]
or, if we want wait_keys to return a sequence of different values, we can use
mock_get_keys.side_effect = [ [],[['space', 1]] ]
(Returns nothing the first time itâs called, then a space. Be sure not to run out of return values.)
Next, we call the run_blocks function, passing it the trials we just set up. The experiment is now run and, due to the mocks, we donât need to press any keys.
Finally, we assert a couple of things about the first trial:
self.assertEqual(t['RT_TO'], 0)
self.assertEqual(t['tone_sdt'], 'HI')
âŚand then the test is finished.
You can define multiple tests like this, each in their own class, and run them all with
if __name__ == '__main__':
unittest.main()
Has anyone else used this approach? The satisfaction you get from watching your experiment complete a full hourâs run on its own - and the confidence that itâs actually collecting the right data, even after changes to the code - are great.