JGiven: Pragmatic BDD for Java

Jan Schäfer

@JanSchfr

Sep 20, 2016

Why BDD?

Typical Test


@Test
public void shouldInsertPetIntoDatabaseAndGenerateId() {
    Owner owner6 = this.clinicService.findOwnerById(6);
    int found = owner6.getPets().size();

    Pet pet = new Pet();
    pet.setName("bowser");
    Collection<PetType> types = this.clinicService.findPetTypes();
    pet.setType(EntityUtils.getById(types, PetType.class, 2));
    pet.setBirthDate(new DateTime());
    owner6.addPet(pet);
    assertThat(owner6.getPets().size()).isEqualTo(found + 1);

    this.clinicService.savePet(pet);
    this.clinicService.saveOwner(owner6);

    owner6 = this.clinicService.findOwnerById(6);
    assertThat(owner6.getPets().size()).isEqualTo(found + 1);
    // checks that id has been generated
    assertThat(pet.getId()).isNotNull();
}
  
Example from github.com/spring-projects/spring-petclinic

Issues with typical tests

  • Many technical and often irrelevant details
  • Point of the test often hard to grasp
  • Code duplication
  • Can only be read by developers
  • Cannot be used as documentation

Behavior-Driven Development

  • Behavior is described in a common domain language understandable by domain experts
  • Domain experts and developers collaborate on defining the behavior
  • Executed like normal tests
  • Creates a living documentation

BDD Example


  Scenario: Pets can be assigned to pet owners

   Given a pet owner
     And a dog
    When assigning the pet to the pet owner
    Then the pet owner owns an additional pet
 

BDD in Java

"Classical" BDD Frameworks

  • Cucumber: Plain Text + Java
  • JBehave: Plain Text + Java
  • Concordion: HTML + Java
  • Fitness: Wiki + Java

Additional maintenance cost

Developer-friendly BDD Frameworks

  • Spock: Groovy
  • ScalaTest: Scala
  • Jnario: Xtend
  • Serenity*: Java

*strongly focused on web testing, shares some concepts with JGiven
 

Stack Overflow?

Goals

  • Developer friendly (low maintenance overhead)
  • Readable test code (Given-When-Then)
  • Modular and reusable test code
  • Reports for domain experts

Demo

Scenarios in JGiven


import org.junit.Test;
import com.tngtech.jgiven.junit.ScenarioTest;

public class FindOwnerTest
       extends ScenarioTest<GivenOwner, WhenSearching, ThenOwner> {

    @Test
    public void owners_can_be_found_by_last_name() {

        given().an_owner_with_last_name("Smith");

        when().searching_for("Smith");

        then().exactly_the_given_owner_is_found();
    }
}
      

Stage Classes


@JGivenStage // only needed when using Spring
public class WhenSearching extends Stage<WhenSearching> {

    @Autowired
    ClinicService clinicService;

    @ScenarioState
    Owner owner;

    public WhenSearching searching_for(String name) {
       owner = this.clinicService.findOwnerByName(name);
       return this;
    }
}
          

Console Output


           Owners can be found by last name

             Given an owner with last name "Smith"
              When searching for "Smith"
              Then exactly the given owner is found

HTML5 App

Practical Experience

  • 3 years of experience in a large Java Enterprise project (up to 70 developers)
  • Over 3000 Scenarios
  • Readability and reusability of test code has been greatly improved
  • Maintenance costs of automated tests have been reduced (no hard numbers)
  • Well accepted by developers
  • Easy to learn by new developers
  • Developers and domain experts collaborate using scenarios

Further Features

Parameterized Scenarios

@Test
@DataProvider({
   "Smith, 1",
   "Davis, 0",
   "Sm, 1"})
public void should_find_owner_by_last_name( String searchTerm, 
	                                    int numberOfResults ) {

    given().an_owner_with_last_name("Smith");

    when().searching_for(searchTerm);

    then().exactly_$_owner_is_found(numberOfResults);
}
Uses the DataProviderRunner (github.com/TNG/junit-dataprovider).
Parameterized Runner and Theories of JUnit are also supported.

Parameterized Scenarios

Console Output

 Should find owner by last name

   Given an owner with last name Smith
    When searching for <searchTerm>
    Then exactly <numberOfResults> owner is found

  Cases:

   | # | searchTerm | numberOfResults | Status  |
   +---+------------+-----------------+---------+
   | 1 | Smith      |               1 | Success |
   | 2 | Davis      |               0 | Success |
   | 3 | Sm         |               1 | Success |

Parameterized Scenarios

HTML Report

Parameter Formatting

  • Default: toString()
  • @Format( MyCustomFormatter.class )
  • @Formatf( " -- %s -- " )
  • @MyCustomFormatAnnotation

Example

@OnOff

@Format( value = BooleanFormatter.class, args = { "on", "off" } )
@Retention( RetentionPolicy.RUNTIME )
@interface OnOff {}

Apply to parameter

public SELF the_machine_is_$( @OnOff boolean onOrOff ) { ... }

Use step

given().the_machine_is_$( false );

Report

Given the machine is off

@BeforeScenario und @AfterScenario


public class GivenSteps extends Stage<GivenSteps> {

    @ProvidedScenarioState
    File temporaryFolder;

    @BeforeScenario
    void setupTemporaryFolder() {
        temporaryFolder = ...
    }

    @AfterScenario
    void deleteTemporaryFolder() {
        temporaryFolder.delete();
    }
}
          

@ScenarioRule


public class TemporaryFolderRule {
    File temporaryFolder;

    public void before() {
        temporaryFolder = ...
    }

    public void after() {
        temporaryFolder.delete();
    }
}

public class GivenSteps extends Stage<GivenSteps> {
    @ScenarioRule
    TemporaryFolderRule rule = new TemporaryFolderRule();
}
          

@AfterStage, @BeforeStage


public class GivenCustomer extends Stage<GivenSteps> {
    CustomerBuilder builder;

    @ProvidedScenarioState
    Customer customer;

    public void a_customer() {
        builder = new CustomerBuilder();
    }

    public void with_age(int age) {
        builder.withAge(age);
    }

    @AfterStage
    void buildCustomer() {
        customer = builder.build();
    }
}
          

Tags

@Test @FeatureEmail
void the_customer_gets_an_email_when_ordering_a_book() {
   ...
}

With Values

@Test @Story( "ABC-123" )
void the_customer_gets_an_email_when_ordering_a_book() { ... }

@Pending

  • Marks whole scenarios or single steps as pending
  • Steps are skipped and marked accordingly

HTML Report

@Hidden

  • Marks methods to not appear in the report
  • Useful for technical methods

@Hidden
public SELF doSomethingTechnical() { ... }

Extended Step Descriptions

@ExtendedDescription("The Hitchhiker's Guide to the Galaxy, "
                   + "by default the book is not on stock" )
public SELF a_book() { ... }

HTML Report

Attachments

public class Html5ReportStage {
    @ExpectedScenarioState
    protected CurrentStep currentStep; // provided by JGiven

    protected void takeScreenshot() {
        String base64 = ( (TakesScreenshot) webDriver )
            .getScreenshotAs( OutputType.BASE64 );
        currentStep.addAttachment(
            Attachment.fromBase64( base64, MediaType.PNG )
                      .withTitle( "Screenshot" ) );
    }
}

HTML Report

Inline Attachments

Source: https://github.com/mthuret/xke-jgiven

Summary

  • Developer friendly
  • Highly modular and reusable test code
  • Just Java, no further language is needed
  • Easy to integrate into existing test infrastructures (JUnit, TestNG)
  • Open Source (Apache 2 Licence)
  • Maven and Jenkins plugins
  • Nice reports for domain experts
  • Domain experts can not write scenarios in JGiven
    (But they can write them in any other format and developers can easily translate them to JGiven)

BDD without the hassle!

Thank You!

@JanSchfr

jgiven.org

github.com/TNG/JGiven

https://janschaefer.github.io/jgiven-slides-javaone-2016/