# encoding: utf-8 """ Simple Attempt at parsing textual expectation in order to generate test case skeletons. Building a model of the expectations in order to generate tests >>> from tspec2test import * >>> context1 = Given('a user of group') >>> context2 = Given('a manager of group') >>> event1 = Event('logged in') >>> event2 = Event('has task todo') >>> ensure1 = Ensure('user can view invoices') >>> ensure2 = Ensure('user can read task details') >>> ensure3 = Ensure('manager can assign tasks to users') >>> spec = Specification( ... [Scenario('1', context1, event1, ensure1), ... Scenario('2', context1, event2, ensure2), ... Scenario('3', context2, event1, ensure3), ... ] ... ) ... >>> print spec2test(spec) import unittest class TestAManagerOfGroup(unittest.TestCase): def test_when_logged_in_manager_can_assign_tasks_to_users(self): pass class TestAUserOfGroup(unittest.TestCase): def test_when_logged_in_user_can_view_invoices(self): pass def test_when_has_task_todo_user_can_read_task_details(self): pass def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestAManagerOfGroup) suite.addTest(unittest.makeSuite(TestAUserOfGroup) return suite if __name__ == '__main__': unittest.main() Generating tests from a textual definition matching the template. >>> tspec = ''' ... Scenario 1: ... Given a user of group ... When logged in ... Ensure user can view invoices ... ... Scenario 2: ... Given a user of group ... When has task todo ... Ensure user can read task details ... ''' >>> parse = TextSpecParser() >>> spec = parse(tspec) >>> print spec2test(spec) import unittest class TestAUserOfGroup(unittest.TestCase): def test_when_logged_in_user_can_view_invoices(self): pass def test_when_has_task_todo_user_can_read_task_details(self): pass def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestAUserOfGroup) return suite if __name__ == '__main__': unittest.main() """ __author__ = 'Raphael.Marvie@lifl.fr' __date__ = '2007-03-16' def _make_name(sentence, separator, word_case): """Returns a name from the plain text sentence.""" return separator.join([word_case(x) for x in sentence.split()]) def wiki_name(sentence): """Returns a WikiName from the plain text sentence.""" return _make_name(sentence, '', str.capitalize) def python_name(sentence): """Returns a python_name from the plain text sentence.""" return _make_name(sentence, '_', str.lower) ###################################################################### # Classes for modeling a specification ###################################################################### class Specification(object): """Representation for a specification definition.""" def __init__(self, scenarios=None): if not scenarios: scenarios = list() self.scenarios = scenarios def __repr__(self): res = list() for scenario in self.scenarios: res.append(str(scenario)) return '\n'.join(res) class Scenario(object): """Representation for a scenario definition.""" def __init__(self, name=None, given=None, when=None, ensure=None): self.name = name self.given = given self.when = when self.ensure = ensure def __repr__(self): return '\n'.join([ 'Scenario %s:' % self.name, 'Given %s' % self.given.desc, 'When %s' % self.when.desc, 'Ensure %s' % self.ensure.desc, ]) class Given(object): """Representation for a context definition.""" def __init__(self, desc): self.desc = desc class Event(object): """Representation for an event definition.""" def __init__(self, desc): self.desc = desc class Ensure(object): """Representation for an ensure definition.""" def __init__(self, desc): self.desc = desc ###################################################################### # Classes for modeling a test suite ###################################################################### class TestSuite(object): """Representation for a test suite definition.""" def __init__(self, tests=None): if not tests: tests = list() self.tests = tests def __repr__(self): """docstring for __repr__""" res = list() res.append('import unittest') for test in self.tests: res.append(str(test)) res.append('def suite():') res.append(' suite = unittest.TestSuite()') for test in self.tests: res.append(' suite.addTest(unittest.makeSuite(Test%s)' % \ test.name) res.append(' return suite') res.append("if __name__ == '__main__': unittest.main()") return '\n'.join(res) class TestCase(object): """Representation for a test case definition.""" def __init__(self, name, functions=None): self.name = name if not functions: functions = list() self.functions = functions def __repr__(self): res = list() res.append('class Test%s(unittest.TestCase):' % self.name) for function in self.functions: res.append(' def test_%s(self): pass' % function) return '\n'.join(res) ###################################################################### # Generator of tests from specification ###################################################################### def spec2test(spec): """Return the test skeleton generated from the spec AST.""" tests = dict() for scenario in spec.scenarios: name = wiki_name(scenario.given.desc) try: test = tests[name] except KeyError: test = TestCase(name) tests[name] = test fct = 'when %s %s' % (scenario.when.desc, scenario.ensure.desc) test.functions.append(python_name(fct)) return str(TestSuite(tests.values())) ###################################################################### # Generator of tests from specification ###################################################################### class TextSpecParser(object): """Trivial parser for textual specification. The textual specification has to match the following template: Scenario 1: Given a user of group When logged in Ensure user can view invoices Scenario 2: Given a user of group When has task todo Ensure user can read task details etc. """ def _initialize(self, tspec): self.lines = tspec.split('\n') self.current = 0 self.spec = Specification() self.given = dict() self.when = dict() self.ensure = dict() def __call__(self, tspec): self._initialize(tspec) while self.current < len(self.lines): while self.current < len(self.lines) and \ not self.lines[self.current].startswith('Scenario'): self.current += 1 if self.current < len(self.lines): self.spec.scenarios.append(self._parse_scenario()) else: break return self.spec def _parse_scenario(self): name = self.lines[self.current][len('Scenario'):].strip() self.current += 1 given = self._parse_given() when = self._parse_when() ensure = self._parse_ensure() return Scenario(name, given, when, ensure) def _parse_given(self): return self._parse_item('Given', Given, self.given) def _parse_when(self): return self._parse_item('When', Event, self.when) def _parse_ensure(self): return self._parse_item('Ensure', Ensure, self.ensure) def _parse_item(self, item, clss, index): if self.current < len(self.lines) and \ self.lines[self.current].startswith(item): name = self.lines[self.current][len(item):].strip() self.current += 1 if name in index: return index[name] else: res = clss(name) index[name] = res return res else: return Given('') ###################################################################### # Bootstrap ###################################################################### if __name__ == '__main__': import doctest doctest.testmod() # eof