diff --git a/approvaltests-tests/src/test/java/org/approvaltests/bdd/PrintableScenarioTest.bddScenarioThatThrows.approved.txt b/approvaltests-tests/src/test/java/org/approvaltests/bdd/PrintableScenarioTest.bddScenarioThatThrows.approved.txt new file mode 100644 index 000000000..c58e27fca --- /dev/null +++ b/approvaltests-tests/src/test/java/org/approvaltests/bdd/PrintableScenarioTest.bddScenarioThatThrows.approved.txt @@ -0,0 +1,26 @@ +Shoplifting +=========== +We expect an exception + +User: Bob + +ShoppingCart: +Articles: + +Subtotal:0 +Shipping:1 +Total:1 + +Something illegal +----------------- +This action threw an exception: java.lang.RuntimeException: shoplifting + +User: Bob + +ShoppingCart: +Articles: + +Subtotal:0 +Shipping:1 +Total:1 + diff --git a/approvaltests-tests/src/test/java/org/approvaltests/bdd/PrintableScenarioTest.java b/approvaltests-tests/src/test/java/org/approvaltests/bdd/PrintableScenarioTest.java new file mode 100644 index 000000000..1d59b7c19 --- /dev/null +++ b/approvaltests-tests/src/test/java/org/approvaltests/bdd/PrintableScenarioTest.java @@ -0,0 +1,119 @@ +package org.approvaltests.bdd; + +import org.approvaltests.Approvals; +import org.approvaltests.strings.Printable; +import org.approvaltests.strings.PrintableScenario; +import org.junit.Test; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class PrintableScenarioTest +{ + // begin-snippet: bdd_test + @Test + public void printBDDScenario() throws Exception + { + PrintableScenario story = new PrintableScenario("Buy Groceries", "Bob puts two items in the shopping cart"); + User currentUser = new User(); + ShoppingCart shoppingCart = new ShoppingCart(currentUser); + story.given(new Printable<>(currentUser, UserPrinter::print), + new Printable<>(shoppingCart, ShoppingCartPrinter::print)); + story.when("Add oranges to the cart", () -> shoppingCart.add("Oranges", 1, BigDecimal.valueOf(5))); + story.when("Add apples to the cart", () -> shoppingCart.add("Apples", 3, BigDecimal.valueOf(4))); + Approvals.verify(story); + } + // end-snippet + @Test + public void bddScenarioThatThrows() throws Exception + { + PrintableScenario story = new PrintableScenario("Shoplifting", "We expect an exception"); + User currentUser = new User(); + ShoppingCart shoppingCart = new ShoppingCart(currentUser); + story.given(new Printable<>(currentUser, UserPrinter::print), + new Printable<>(shoppingCart, ShoppingCartPrinter::print)); + story.when("Something illegal", () -> { + throw new RuntimeException("shoplifting"); + }); + Approvals.verify(story); + } +} + +class User +{ + public String name = "Bob"; +} + +class Item +{ + public String name; + public Integer quantity; + public BigDecimal price; + public BigDecimal amount() + { + return price.multiply(BigDecimal.valueOf(quantity)); + } + public Item(String itemName, int quantity, BigDecimal price) + { + this.name = itemName; + this.quantity = quantity; + this.price = price; + } +} + +class ShoppingCart +{ + private final User user; + public List articles = new ArrayList<>(); + public ShoppingCart(User currentUser) + { + this.user = currentUser; + } + public BigDecimal subtotal() + { + return articles.stream().map(Item::amount).reduce(BigDecimal.ZERO, BigDecimal::add); + } + public BigDecimal shipping() + { + if (Objects.equals(user.name, "Bob")) + { return BigDecimal.ONE; } + return BigDecimal.ZERO; + } + public BigDecimal total() + { + return this.subtotal().add(this.shipping()); + } + public void add(String itemName, int quantity, BigDecimal price) + { + this.articles.add(new Item(itemName, quantity, price)); + } +} + +class ShoppingCartPrinter +{ + public static String print(ShoppingCart shoppingCart) + { + StringBuilder result = new StringBuilder(); + result.append("ShoppingCart:\n"); + result.append("Articles:\n"); + result.append(shoppingCart.articles.stream() + .map(article -> " " + article.name + " price: " + article.price + " quantity: " + article.quantity) + .collect(Collectors.joining("\n"))); + result.append("\n"); + result.append("Subtotal:" + shoppingCart.subtotal() + "\n"); + result.append("Shipping:" + shoppingCart.shipping() + "\n"); + result.append("Total:" + shoppingCart.total() + "\n"); + return result.toString(); + } +} + +class UserPrinter +{ + public static String print(User user) + { + return "User: " + user.name + "\n"; + } +} \ No newline at end of file diff --git a/approvaltests-tests/src/test/java/org/approvaltests/bdd/PrintableScenarioTest.printBDDScenario.approved.txt b/approvaltests-tests/src/test/java/org/approvaltests/bdd/PrintableScenarioTest.printBDDScenario.approved.txt new file mode 100644 index 000000000..16406054b --- /dev/null +++ b/approvaltests-tests/src/test/java/org/approvaltests/bdd/PrintableScenarioTest.printBDDScenario.approved.txt @@ -0,0 +1,36 @@ +Buy Groceries +============= +Bob puts two items in the shopping cart + +User: Bob + +ShoppingCart: +Articles: + +Subtotal:0 +Shipping:1 +Total:1 + +Add oranges to the cart +----------------------- +User: Bob + +ShoppingCart: +Articles: + Oranges price: 5 quantity: 1 +Subtotal:5 +Shipping:1 +Total:6 + +Add apples to the cart +---------------------- +User: Bob + +ShoppingCart: +Articles: + Oranges price: 5 quantity: 1 + Apples price: 4 quantity: 3 +Subtotal:17 +Shipping:1 +Total:18 + diff --git a/approvaltests/docs/how_to/BddScenarios.md b/approvaltests/docs/how_to/BddScenarios.md new file mode 100644 index 000000000..d072d8e13 --- /dev/null +++ b/approvaltests/docs/how_to/BddScenarios.md @@ -0,0 +1,31 @@ + + +# How to make a BDD Scenario using Approvals + +## Contents + +* [Introduction](#introduction) +* [Sample Code](#sample-code) + +## Introduction +The idea is that you will end up with a descriptive approved file that reads like a BDD Scenario. You can show this to your less-technical collaborators, and they should be able to understand what scenario is being tested without needing to read the test sourcecode. + +It doesn't use Gherkin strictly, but you still get a scenario with a name, description, and Given, When, Then steps. + +## Sample Code + +This test case uses a PrintableScenario: + + + + + +* Construct the scenario at the beginning of the test with a descriptive name and summary. +* When you are done with the code for the "given" step of the test, call story.given() with your list of Printables. Use one printable to wrap each object you have created which you think might change state during the test and that you want to verify. +* In each "When" step of your test, call when() with a descriptive name for what the user is doing. The second argument is a lambda that contains code to actually do the 'when' step. This is optional, you can write it in your test as normal code before the call to 'when' if you prefer. +* The "Then" step is a call to then() which you pass to Approvals.Verify. This will verify the state of all the Printables as they changed throughout the course of the test. + +This produces a feature file like this one: [[PrintableScenarioTest.printBDDScenario.approved.txt](../../../approvaltests-tests/src/test/javavorg/approvaltests/bdd/PrintableScenarioTest.printBDDScenario.approved.txt)] + +Note - if you don't want to use the BDD terminology you don't have to. You can equally well use 'arrange', 'act', 'print' instead of 'given' 'when' 'then' + diff --git a/approvaltests/src/main/java/org/approvaltests/strings/PrintableScenario.java b/approvaltests/src/main/java/org/approvaltests/strings/PrintableScenario.java new file mode 100644 index 000000000..080e9031d --- /dev/null +++ b/approvaltests/src/main/java/org/approvaltests/strings/PrintableScenario.java @@ -0,0 +1,91 @@ +package org.approvaltests.strings; + +import org.lambda.actions.Action0WithException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +public class PrintableScenario +{ + private final StringBuilder toVerify = new StringBuilder(); + private final String name; + private final String description; + private final ArrayList printables = new ArrayList<>(); + public PrintableScenario(String name, String description) + { + this.name = name; + this.description = description; + } + public PrintableScenario(String name) + { + this(name, ""); + } + public void given(Printable... printables) + { + arrange(printables); + } + public void arrange(Printable... printables) + { + this.printables.addAll(Arrays.asList(printables)); + toVerify.append(makeHeading(name, description, "=")); + toVerify.append(printAll()); + } + public String makeHeading(String heading, String summary, String underlineCharacter) + { + int count = heading.length(); + // create a string made up of n copies of underlineCharacter + String underlineHeading = String.join("", Collections.nCopies(count, underlineCharacter)); + String result = heading + "\n" + underlineHeading + "\n"; + if (summary != null && !summary.isEmpty()) + { + result += summary + "\n\n"; + } + return result; + } + public String printAll() + { + StringBuilder result = new StringBuilder(); + for (Printable printable : this.printables) + { + result.append(printable.toString()); + result.append("\n"); + } + return result.toString(); + } + public void when(String action, Action0WithException function) + { + try + { + function.call(); + when(action); + } + catch (Throwable e) + { + toVerify.append(makeHeading(action, "This action threw an exception: " + e, "-")); + toVerify.append(printAll()); + } + } + public void when(String action) + { + act(action); + } + public void act(String action) + { + toVerify.append(makeHeading(action, "", "-")); + toVerify.append(printAll()); + } + public String then() + { + return print(); + } + public String print() + { + return toVerify.toString(); + } + @Override + public String toString() + { + return print(); + } +}