Last active
July 6, 2019 12:56
-
-
Save pablochacin/ed48bd27e09932a0c997821f1a6fffc8 to your computer and use it in GitHub Desktop.
A minimalist BDD framework with fluent api
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Fluent BDD | |
Fluent BDD is a minimalistic testing framework with a fluent API. | |
A Scenario is defined as a sequence of Conditions, followed by | |
a series of Events and the Clauses that must be satiesfied at | |
the end. Multiple Conditions, Events and Clauses can be specified. | |
## Fluent API | |
The API for defining a test scenario follows a fluent style. | |
Function calls can be chanined in a close-to-english syntax. | |
Example: | |
``` | |
Scenario("Test scenario").Given(f1, 1, 'A').When(f2,1, a=3, b=2).Then(f3).Is(True).Run() | |
``` | |
Multiple Conditions, Events and Clauses can be chained using the 'And' connector: | |
Example: | |
``` | |
Scenario("Multiple conditions") \ | |
.Given(f1, 1, 'B').And(f1, 1, 'A') \ | |
.When(f2, a=3, b=1) \ | |
.Then(f3).Is(True) \ | |
.Run() | |
## Function invocation | |
Actions, Events and Clauses receive a function as first parameter, | |
followed by a (potentially empty) sequence of arguments, and | |
an optional series of named arguments. | |
The functions used in the statements can be static methods or instance method. | |
Example: | |
``` | |
class SUT: | |
def f(self): | |
return | |
def v(self): | |
return True | |
def verify(sut): | |
return sut.v() | |
s = SUT() | |
Scenario("Class methods").when(s.f).then(verify,s).Is(True).Run() | |
``` | |
## Clauses and assertions | |
Clauses are defined as a invocation of a function followed by an assertion on the returning value. | |
Supported assertions are: | |
* Is(value) | |
* IsNot(value) | |
## Reusing scenarios | |
In some cases, multiple scenarios share the same Conditions or values. In that case, | |
it is possible to define a base scenario and use it as a basis for other scenarios. | |
Events, and Clauses, can also be copied. | |
For reusing a scenario, it must be built (not executed) | |
Example: | |
``` | |
base = Scenario("Base") \ | |
.Given(f,a, b) \ | |
.Given(g,1) \ | |
.Build() | |
Scenario("Test 1") \ | |
.WithConditionsFrom(base) \ | |
.When(f1,'c') \ | |
.Then(f2).Is(True) \ | |
.Run() | |
Scenario("Test 2") \ | |
.WithConditionsFrom(base) \ | |
.When(f,'a') \ | |
.Then(f2).Is(False) \ | |
.Run() | |
``` | |
## Using values | |
Sometimes is convenient to execute the same test Scenario with multiple arguments for | |
the different statements. The `WithValues` function allows setting a list of named | |
parameters which can be used in any statement. Named arguments are references using | |
its name enclosed in '{}'. | |
Example: | |
``` | |
Scenario("With Values") \ | |
.AsVerbose() | |
.WithValues(('a', 'b','c','d'), \ | |
[(1,2,True,True), \ | |
(2,3,True,False)]) \ | |
.Given(sut.f0) \ | |
.When(sut.f2, '{a}', 'b') \ | |
.Then(echo, '{c}').Is('{d}') \ | |
.Run() | |
Executing 'With Values' with values {'a': 1, 'b': 2, 'c': True, 'd': True} | |
Executing condition f0() | |
Executing event f2(1,b) | |
Executing clause echo(True) | |
Scenario 'With Values' success | |
Executing 'With Values' with values {'a': 2, 'b': 3, 'c': True, 'd': False} | |
Executing condition f0() | |
Executing event f2(2,b) | |
Executing clause echo(True) | |
Scenario 'With Values' failed: Clause echo(True) failed with error: assertion Is(True,False) failed | |
``` | |
Notice the function `f2` is called with the literal `b` as second argument while | |
the first argument `{a}` is replaced on each execution with the correspoding | |
value. | |
(c) 2019 Pablo Chacin |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Scenario: | |
class Condition: | |
def __init__(self, scenario): | |
self.scenario = scenario | |
def And(self, condition, *args, **kwargs): | |
self.scenario.add_condition(condition, args, kwargs) | |
return Scenario.Condition(self.scenario) | |
def When(self, event, *args, **kwargs): | |
self.scenario.add_event(event, args, kwargs) | |
return Scenario.Event(self.scenario) | |
def Build(self): | |
return self.scenario | |
class Event: | |
def __init__(self, scenario): | |
self.scenario = scenario | |
def And(self, event, *args, **kwargs): | |
self.scenario.add_event(event, args, kwargs) | |
return Scenario.Event(self.scenario) | |
def Then(self, clause, *args, **kwargs): | |
return Scenario.OpenClause(self.scenario, clause, args, kwargs) | |
def Build(self): | |
return self.scenario | |
class OpenClause: | |
def __init__(self, scenario, clause, args, kwargs): | |
self.scenario = scenario | |
self.clause = clause | |
self.args = args | |
self.kwargs = kwargs | |
def Is(self, value): | |
self.scenario.add_clause(self.clause, self.args, self.kwargs, Scenario.Assertion.Is, value) | |
return Scenario.Clause(self.scenario) | |
def IsNot(self, value): | |
self.scenario.add_clause(self.clause, self.args, self.kwargs, Scenario.Assertion.IsNot, value) | |
return Scenario.Clause(self.scenario) | |
class Clause: | |
def __init__(self, scenario): | |
self.scenario = scenario | |
def And(self, clause, *args, **kwargs): | |
return Scenario.OpenClause(self.scenario, clause, args, kwargs) | |
def Run(self): | |
self.scenario.Run() | |
def Build(self): | |
return self.scenario | |
class Assertion: | |
@staticmethod | |
def Is(expected, actual): | |
return expected == actual | |
@staticmethod | |
def IsNot(expected, actual): | |
return expected != actual | |
def __init__(self, title): | |
self.title = title | |
self.conditions = [] | |
self.events = [] | |
self.clauses = [] | |
self.values = [] | |
def add_condition(self, condition, args, kwargs): | |
if not condition.__call__: | |
raise ValueError("Condition must be a callable") | |
self.conditions.append((condition, args, kwargs)) | |
def add_event(self, event, args, kwargs): | |
if not event.__call__: | |
raise ValueError("Condition must be a callable") | |
self.events.append((event, args, kwargs)) | |
def add_clause(self, clause, args, kwargs, assertion, value): | |
if not clause.__call__: | |
raise ValueError("Condition must be a callable") | |
self.clauses.append((clause, args, kwargs, assertion, value)) | |
def Given(self, condition, *args, **kwargs): | |
self.add_condition(condition, args, kwargs) | |
return Scenario.Condition(self) | |
def When(self, event, *args, **kwargs): | |
self.add_event(event, args, kwargs) | |
return Scenario.Event(self) | |
def WithConditionsFrom(self, parent): | |
if not isinstance(parent, Scenario): | |
raise ValueError("Parent must be a scenario") | |
self.conditions.extend(parent.conditions) | |
return self | |
def WithEventsFrom(self, parent): | |
if not isinstance(parent, Scenario): | |
raise ValueError("Parent must be a scenario") | |
self.events.extend(parent.events) | |
return self | |
def WithClausesFrom(self, parent): | |
if not isinstance(parent, Scenario): | |
raise ValueError("Parent must be a scenario") | |
self.clauses.extend(parent.clauses) | |
return self | |
def Build(self): | |
return self | |
def WithValues(self, arg_names, values): | |
for index, args in enumerate(values): | |
if len(arg_names) != len(args): | |
raise ValueError("Value tuple {} does not match number of argument names".format(index)) | |
self.values = values | |
self.args_map = {} | |
for arg_pos, arg in enumerate(arg_names): | |
if not isinstance(arg, str): | |
raise ValueError("Value names must be strings") | |
if arg in self.args_map: | |
raise ValueError("Value names must be unique: {}".format(arg)) | |
self.args_map[arg] = arg_pos | |
return self | |
def Run(self): | |
if self.values: | |
runs = len(self.values) | |
else: | |
runs = 1 | |
for r in range(runs): | |
try: | |
self.current_run = r | |
if len(self.events) == 0: | |
raise ValueError("No events specified for scenario {}".format(self.title)) | |
if len(self.clauses) == 0: | |
raise ValueError("No clauses specified for scenario {}".format(self.title)) | |
for condition, args, kwargs in self.conditions: | |
try: | |
self._execute(condition, args, kwargs) | |
except Exception as ex: | |
condition_str = self._signature(condition, args, kwargs) | |
raise Exception("Condition {} Failed with error: {}".format(condition_str, ex)) | |
for event, args, kwargs in self.events: | |
try: | |
self._execute(event, args, kwargs) | |
except Exception as ex: | |
event_str = self._signature(event, args, kwargs) | |
raise Exception("Event {} Failed with error: {}".format(event_str, ex)) | |
for clause, args, kwargs, assertion, value in self.clauses: | |
clause_str = self._signature(clause, args, kwargs) | |
result = None | |
try: | |
result = self._execute(clause, args, kwargs) | |
except Exception as ex: | |
raise Exception("Clause {} Failed with error: {}".format(clause_str, ex)) | |
assertion_value = self._get_value(value) | |
if not assertion(result, assertion_value): | |
raise Exception("Clause {} {} '{}' Actual '{}' ".format(clause_str, assertion.__name__, assertion_value, result)) | |
print("Scenario {} success".format(self.title)) | |
except Exception as ex: | |
print("Scenario {} failed: {}".format(self.title, ex.message)) | |
finally: | |
self.current_run = 0 | |
def _execute(self, func, args, kwargs): | |
args_values, kwargs_values = self._get_values(args, kwargs) | |
return func(*args_values, **kwargs_values) | |
def _get_value(self, arg): | |
if not self.values: | |
return arg | |
if arg not in self.args_map: | |
raise ValueError("Arg not defined {}".format(arg)) | |
return self.values[self.current_run][self.args_map[arg]] | |
def _get_values(self, args, kwargs): | |
if not self.values: | |
return args, kwargs | |
args_values = [] | |
for arg in args: | |
if not arg in self.args_map: | |
raise ValueError("Argument {} not defined".format(arg)) | |
arg_pos = self.args_map[arg] | |
args_values.append(self.values[self.current_run][arg_pos]) | |
kwargs_values = {} | |
for arg_name, arg_value in kwargs.items(): | |
if not arg_name in self.args_map: | |
raise ValueError("Argument {} not defined".format(arg_name)) | |
arg_pos = self.args_map[arg_value] | |
kwargs_values[arg_name] = self.values[0][arg_pos] | |
return tuple(args_values), kwargs_values | |
def _signature(self, function, args, kwargs): | |
args_values, kwargs_values = self._get_values(args, kwargs) | |
signature = "{}(".format(function.__name__) | |
if args: | |
signature = signature + (",".join(["%s"]*len(args_values)) % args_values) | |
if kwargs: | |
signature = signature + ','.join('{}={}'.format(k,v) for k,v in kwargs_values.items()) | |
signature = signature+")" | |
return signature |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from bdd import Scenario | |
def echo(a): | |
return a | |
class SUT: | |
def f0(self): | |
print("Executing f0()") | |
def f1(self, a): | |
print("Executing f1({})".format(a)) | |
def f2(self, a, b): | |
print("Executing f2({},{})".format(a, b)) | |
def fkw(self, a=None, b=None, c=None): | |
print("Executing fkw(a={}, b={}, c={})".format(a, b, c)) | |
def fail(self): | |
print("Executing fail()") | |
raise Exception("Fail!") | |
def main(): | |
sut = SUT() | |
Scenario("Success Test").Given(sut.f0).When(sut.f2, 1, 2).Then(echo, True).Is(True).Run() | |
Scenario("With Values").WithValues(('a', 'b','c'), [(1,2,True), (2,3,False)]).Given(sut.f0).When(sut.f2, 'a', 'b').Then(echo, 'c').Is('c').Run() | |
Scenario("No Conditions").When(sut.f2, 1, 2).Then(echo, True).Is(True).Run() | |
Scenario("Multiple Conditions").Given(sut.f0).And(sut.f1,'a').When(sut.f2, 1, 2).Then(echo, True).Is(True).Run() | |
Scenario("Kwargs").When(sut.fkw, a=1, b=2, c=3).Then(echo, True).Is(True).Run() | |
Scenario("Invalid Kwargs").When(sut.fkw, a=1, b=2, d=3).Then(echo, True).Is(True).Run() | |
parent = Scenario("Parent").Given(sut.f0).And(sut.f2,'a','b').Build() | |
Scenario("Copying Conditions").WithConditionsFrom(parent).When(sut.f2, 1, 2).Then(echo, True).Is(True).Run() | |
Scenario("Failing Assertion").Given(sut.f0).When(sut.f2, 1, 2).Then(echo, False).IsNot(False).Run() | |
Scenario("Failing condition").Given(sut.f0).When(sut.fail).Then(echo, True).Is(True).Run() | |
Scenario("Wrong function arity").Given(sut.f0).When(sut.f1,1,2).Then(echo, True).Is(True).Run() | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment