Note: Currently I'm experimenting a lot. So don't expect a stable API, just yet.
To give you an idea what Bedede is:
- It's a BDD-test framework.
- But, with a fluent API test framework, not a framework for text based BDD specifications
- Hence, the BDD keyworks given, when, and then are methods in the API.
- The purpose is to make your test code more readable
- It supports unit tests
- It supports state-based acceptance tests.
- Supports Selenium, Mockito, Hamcrest
A problem with many unit tests is that it's hard to tell what is the prepare (given), and what is the execution (when). Normally the test starts with a lot of local variable declarations, and some method calls. This framework tries to solve that, making the tests read better.
This is what behavior expressions looks like:
given(a(new BowlingGame()))
.when(performing(the -> the.roll(1))).times(20)
.then(the -> the.score(), is(20));Behavior expressions use Hamcrest matchers, as you can see in the example above (last line: the "is(20)"). One thing about behavior expressions is that you don't need to declare any local variables, and you need a lot less helper functions, since the code reads well as it is.
given(a(new Person())).with(it -> {
it.setFirstName("John");
it.setFamilyName("Smith");
})
.when(retrieving(Person::getFullName))
.then(it(), is("John Smith"));Java 8 has a new feature called Streams, which also works nicely with the framework. This is an example with streams:
import static com.github.thogr.bedede.Expressions.*;
public class PokerTest {
@Test
public void shouldSortCardsAccordingToRank() {
given(a(Stream.of("10H", "1H", "KD", "QS")))
.when(transforming(it->it.map(Card::new).sorted()))
.when(transforming(it->it.map(Card::toString).collect(toList())))
.then(it(), is(equalTo(asList("1H", "10H", "QS", "KD"))));
}
}Behavior expressions are really simple. You start with an object, the given(...), possibly with at with()-clause to set it up. Then you perhaps transform it to something else, with when(transforming(...)), or retrieve something from it with the when(retrieving(...)), possibly in several when-steps. And lastly you verify the result, with a Hamcrest matcher in the then-clause:
given(a(<object>))
.when(transforming(....))
.when(retrieving(....))
.then(it(), <matcher>)Of course you may have as many when() and then() as you like. Or perhaps you just perform operations on the objects, that will change its internal state, and verify the state lastly, in the then-clause:
given(a(<object>))
.when(performing(....))
.then(<function>, <matcher>)Naturally, you may combine these any way you like. The main difference is that transforming() will give you a new object to operate on, and when(performing()) will keep the same object.
Also, the Hamcrest matcher is optional, if you call a boolean method (i.e. a predicate):
given(a(new Counter()))
.then(it -> it.isStopped());As you can see, Java 8 lambda expressions are used. But, since Java has Method references as well, you don't have to use lambda expressions. The above expression could have been written:
given(a(new Counter()))
.then(Counter::isStopped);And for example:
given(a(new Counter()))
.when(performing(it -> it.start(2)))
.when(performing(it -> it.decrease()))
.then(it -> it.isStopped(), is(false));can be written:
given(a(new Counter()))
.when(performing(it -> it.start(2)))
.when(performing(Counter::decrease))
.then(Counter::isStopped, is(false));You may even write it:
given(a(new Counter())).and(a(2))
.when(performing(Counter::start))
.when(performing(Counter::decrease))
.then(Counter::isStopped, is(false));You can also reuse a behavior expression in several tests, e.g. to initialize several tests with the same "given" (or a parametrized one), or just extract it for better readability:
@Test
public void shouldBeStoppedWhenStartedAndDecreasedToZero() {
given(startedWith(3))
.when(decreasing(3))
.then(Counter::isStopped);
}
private BehaviorExpression<Counter> startedWith(int startval) {
return given(a(new Counter())).and(a(startval))
.when(performing(Counter::start));
}
private Performing<Counter> decreasing(int n) {
return performing(Counter::decrease).times(n);
}
private BehaviorExpression<Counter> initial() {
return
given(a(new Counter()));
}====== Uncle Bob said:
The Given/When/Then syntax of BDD seemed eerily familiar when I first heard about it several years ago. It’s been tickling at the back of my brain since then. Something about that triplet was trying to resonate with something else in my brain. This strange similarity caused me to realize that GIVEN/WHEN/THEN is simply a state transition, and that BDD is really just a way to describe a finite state machine.
- The framework includes support for so called "state-based" tests.
- It's designed by the assumption that BDD is really about describing transitions between states in a state machine.
- It's very much like Model-based testing, but without the auto-generated tests
State-based testing is when you define the expected behavior of a system in terms of a state machine. That's just like model based testing. But in this framework you define the states as classes, and its actions as methods. The explicit tests are the written as given-when-then rules.
The
given(S1.class).when(it->it.action()).then(S2.class)syntax defines an expected transition from state S1 to S2 when theaction()is performed. As theaction()method is defined in the S1 class it can only be performed in state S1.
Example:
given(LockedDoorState.class)
.when(it -> it.turnsKey())
.then(UnlockedDoorState.class);For testing web applications:
given(GoogleSearchPage.class)
.when(it -> it.searchesFor("Selenium"))
.then(GoogleResultPage.class)
.then(it -> it.hasTitle("Selenium"));import static com.github.thogr.bedede.Expressions.*;
@InitialState
public class DoorLocked extends DoorState {
@OnEntry
public Expecting<BooleanCondition> shouldBeLocked() {
return expecting(door.isLocked(), otherwise("Unexpected unlocked door"));
}
}The @OnEntry method is the guard that defines the entry condition that must be fulfilled when entering the state.
An Entry to a state defines the path to the state, as a given().when().then() transition.
By defining a @DefaultEntry in a state class you may access the state's default entry with the given(State.class) syntax.
But you may also use an entry explicitly, e.g given(DoorUnlocked.byUnlockingWith(someKey)
public static class DoorUnlocked extends DoorState {
@DefaultEntry
public static Entry<DoorUnlocked> byUnlockingWithCorrectKey() {
return byUnlockingWith(CORRECT_KEY);
}
public static final Entry<DoorUnlocked> byUnlockingWith(final Key key) {
return entry(DoorUnlocked.class).as()
.given(DoorLocked.class)
.when(it -> it.turnsKey(key))
.then(DoorUnlocked.class);
}
@OnEntry
public Expecting<BooleanCondition> shouldBeUnlocked() {
return expecting(!door.isLocked(), otherwise("Unexpected locked door"));
}
}import static com.github.thogr.bedede.Expressions.*;
public class DoorExampleTest {
//.....
@Test
public void shouldLockWithCorrectKey() {
given(DoorUnlocked.class)
.when(it -> it.turnsKey(CORRECT_KEY))
.then(DoorLocked.class);
}
@Test
public void shouldNotLockWithWrongKey() {
given(DoorUnlocked.class)
.when(it -> it.turnsKey(WRONG_KEY))
.then(DoorUnlocked.class);
}
}repositories {
maven {
url "http://dl.bintray.com/thogr/maven"
}
}
dependencies {
testCompile "com.github.thogr.bedede:bedede-lib:0.10.2"
}