Tuesday, May 27, 2014

Behaviour Driven Development using Cucumber in Java

there's a conversation between a partner and a business analyst (ba)
partner : bro, you need to encrypt your data before you send it to my service
ba : ok, what kind of encryption?
partner : we use AES encryption
ba : ok sir, please give me an example of the original text and what's the correct encryption result.
partner : try this. 'BAHLUL ENTE' should be encrypted as '/0YQIBztk6sqIRiw/Bh3Mw=='
partner : use 'DEFAULT_ENCRYPTOR_KEY_1234' as the encryption key.
ba : are you sure with that one sir?
partner : off course. that's how it should be.

ok, so the partner need an additional feature for the integration, to encrypt the data before it's sent to his service.

lets break it down.
feature: text encryption using AES algorithm
test scenario: we have text 'BAHLUL ENTE', encrypt it with key 'DEFAULT_ENCRYPTOR_KEY_1234' and it should resulted '/0YQIBztk6sqIRiw/Bh3Mw=='.

so, how to make it easy to explain the developer about this?
is there any tools that can be easily understandable for both the partner to check and the developer to code?

it's cucumber.

the main idea is to have a file that contains every requirement and test scenarios (and off course it should be human readable), and there's a unit testing program that reads the file, and runs every single step in the scenario. and assert if the program runs well according to the provision.

ok, where to start?

first, create a text file, for this example name it as encryption.feature, that will be the reference of the scenarios.
 Feature: text encryption using AES algorithm  
 
 Scenario: Encrypting text using AES encryption algorithm  
 Given the encryption algorithmm is "AES"  
 And we set the encryptor key "DEFAULT_ENCRYPTOR_KEY_1234"  
 When we give text "BAHLUL ENTE"  
 Then the result should be "/0YQIBztk6sqIRiw/Bh3Mw==" 
 And the decryption should bring "/0YQIBztk6sqIRiw/Bh3Mw==" back to "BAHLUL ENTE" 

from this point, let's move to developer's point of view.

that file contains Gherkin syntax, which is 'Given', 'When', 'Then', 'And', there is also one more that we don't use here is 'But'. you can see the explanation of each of them here https://github.com/cucumber/cucumber/wiki/Given-When-Then.

how to read it from java?

as usual unit test, we can use junit. also we need a library that 'integrate' cucumber and junit, and also cucumber and java (at first cucumber is developed for ruby language).

if we use maven, we simple add these dependencies:
 <dependency>  
      <groupId>info.cukes</groupId>  
      <artifactId>cucumber-java</artifactId>  
      <version>1.0.0</version>  
      <scope>test</scope>  
 </dependency>  
 <dependency>  
      <groupId>info.cukes</groupId>  
      <artifactId>cucumber-junit</artifactId>  
      <version>1.0.0</version>  
      <scope>test</scope>  
 </dependency>  
 <dependency>  
      <groupId>junit</groupId>  
      <artifactId>junit</artifactId>  
      <version>4.10</version>  
      <scope>test</scope>  
 </dependency>  

then we need to create a junit runner that will run the test. we name it org.mazb.encr.RunTest.java and put it under src/test/java.
 import org.junit.runner.RunWith;  
 import cucumber.junit.Cucumber; 
 
 @RunWith(Cucumber.class)  
 @Cucumber.Options ( format = { "pretty" } )  
 public class RunTest {  

 }  

@Cucumber annotation tells the program to run the Gherkin syntax in the feature file somewhere in the classpath. default location is under src/test/resources and under the same package with the runner (org.mazb.encr), so we put the feature file there.
format = { "pretty" } attribute tells the program to verbose the test in the console.

can we run the test now? yes we can, let's see what happens.
we simply run it with mvn test. and here's what happens:
 Running org.mazb.encr.RunTest  
 
 Feature: text encryption using AES algorithm  
  Scenario: Encrypting text using AES encryption algorithm  
   Given the encryption algorithmm is "AES"  
   And we set the encryptor key "DEFAULT_ENCRYPTOR_KEY_1234"  
   When we give text to encrypt "BAHLUL ENTE"  
   Then the encryption result should be "/0YQIBztk6sqIRiw/Bh3Mw=="  
   And the decryption should bring "/0YQIBztk6sqIRiw/Bh3Mw==" back to "BAHLUL ENTE"  
 
 You can implement missing steps with the snippets below:  
 @Given("^the encryption algorithmm is \"([^\"]*)\"$")  
 public void the_encryption_algorithmm_is(String arg1) {  
   // Express the Regexp above with the code you wish you had  
   throw new PendingException();  
 } 
 
 @Given("^we set the encryptor key \"([^\"]*)\"$")  
 public void we_set_the_encryptor_key(String arg1) {  
   // Express the Regexp above with the code you wish you had  
   throw new PendingException();  
 } 
 
 @When("^we give text to encrypt \"([^\"]*)\"$")  
 public void we_give_text_to_encrypt(String arg1) {  
   // Express the Regexp above with the code you wish you had  
   throw new PendingException();  
 }  

 @Then("^the encryption result should be \"([^\"]*)\"$")  
 public void the_encryption_result_should_be(String arg1) {  
   // Express the Regexp above with the code you wish you had  
   throw new PendingException();  
 }  

 @Then("^the decryption should bring \"([^\"]*)\" back to \"([^\"]*)\"$")  
 public void the_decryption_should_bring_back_to(String arg1, String arg2) {  
   // Express the Regexp above with the code you wish you had  
   throw new PendingException();  
 }  

it tells us that we haven't define the steps in the scenario. we need to implement every single line under Gherkin keywords into java statements so it can be run programatically.

so we create a java class, under src/test/java, and implement the steps. for example org.mazb.encr.EncryptionStepDefinition.java. we create a method for each step in the feature file, and put the suitable annotation (Given/When/Then) on it. Cucumber will scan the classes under src/test/java that contains the right step based on the feature file.

for example we use this class:
 import static org.junit.Assert.assertTrue;  

 import org.apache.commons.lang.StringUtils;  

 import org.mazb.encr.AesEncryptor;  

 import cucumber.annotation.en.Given;  
 import cucumber.annotation.en.Then;  
 import cucumber.annotation.en.When;  

 public class EncryptionStepDefinition {  

      private AesEncryptor encryptor = new AesEncryptor();  

      @Given("^the encryption algorithmm is \"([^\"]*)\"$")  
      public void encryptionAlgorithm(String algo){  
           encryptor.setAlgorithmName(algo);  
      }  

      @Given("^we set the encryptor key \"([^\"]*)\"$")  
      public void encryptorKey(String encryptorKey){  
           encryptor.setEncryptorKey(encryptorKey);  
      }  

      @When("^we give text to encrypt \"([^\"]*)\"$")  
      public void setText(String text){  
           encryptor.setText(text);  
      }  

      @Then("^the encryption result should be \"([^\"]*)\"$")  
      public void getResult(String result) throws Exception{  
           assertTrue(StringUtils.equals(encryptor.encrypt(), result));  
      }  

      @Then("^the decryption should bring \"([^\"]*)\" back to \"([^\"]*)\"$")  
      public void getDecryptionResult(String encrypted, String original) throws Exception{  
           encryptor.setText(encrypted);  
           assertTrue(StringUtils.equals(encryptor.decrypt(), original));  
      }  
 }  

note: AesEncryptor is the encryption class that we want to test. assuming we already create the class and now want to test it.

all the methods in this class have annotation from cucumber.annotation.en.*. each description inside the annotation must suites the steps defined in feature file, and for the value we're gonna test, we can use regex in the description.

for example,
@Given("^the encryption algorithmm is \"([^\"]*)\"$")
must have the same pattern with
Given the encryption algorithmm is "AES"
in the feature file so cucumber can get it right.

([^\"]*) is a regular expression, the spec can be found else wehere, here's for example: http://www.vogella.com/tutorials/JavaRegularExpressions/article.html. the regex also will be argument to the method, so the string defined with that regex will be accepted as args[0] in the method. see also the last method which has 2 regex (passes 2 arguments).

can we run the test now? yes we can, let's see what happens.
 Running org.mazb.encr.RunTest  
 Feature: text encryption using AES algorithm  

  Scenario: Encrypting text using AES encryption algorithm  
   Given the encryption algorithmm is "AES"  
   And we set the encryptor key "DEFAULT_ENCRYPTOR_KEY_1234"  
   When we give text to encrypt "BAHLUL ENTE"        
   Then the encryption result should be "/0YQIBztk6sqIRiw/Bh3Mw=="  
    java.lang.AssertionError  
         at org.junit.Assert.fail(Assert.java:92)  
         at org.junit.Assert.assertTrue(Assert.java:43)  
         at org.junit.Assert.assertTrue(Assert.java:54)  
         at org.mazb.encr.EncryptionStepDefinition.getResult(EncryptionStepDefinition.java:33)  
         at ✽.Then the encryption result should be "/0YQIBztk6sqIRiw/Bh3Mw=="(org/mazb/encr/encryption.feature:7)  

   And the decryption should bring "/0YQIBztk6sqIRiw/Bh3Mw==" back to "BAHLUL ENTE" # EncryptionStepDefinition.getDecryptionResult(String,String)  

 java.lang.AssertionError  
      at org.junit.Assert.fail(Assert.java:92)  
      at org.junit.Assert.assertTrue(Assert.java:43)  
      at org.junit.Assert.assertTrue(Assert.java:54)  
      at org.mazb.encr.EncryptionStepDefinition.getResult(EncryptionStepDefinition.java:33)  
      at ✽.Then the encryption result should be "/0YQIBztk6sqIRiw/Bh3Mw=="(org/mazb/encr/encryption.feature:7)  

oops! what's going on? seems like there's something wrong with the encryption. cucumber tells us any false assert per step. in this case, the problem exists in the step 'Then the encryption result should be "/0YQIBztk6sqIRiw/Bh3Mw=="' which means our encryption returns the wrong result from the provision. let's check again the code, and fix it.

after it fixed, let's run the test again
 Running org.mazb.encr.RunTest  
 Feature: text encryption using AES algorithm  

  Scenario: Encrypting text using AES encryption algorithm  
   Given the encryption algorithmm is "AES"  
   And we set the encryptor key "DEFAULT_ENCRYPTOR_KEY_1234"  
   When we give text to encrypt "BAHLUL ENTE"  
   Then the encryption result should be "/0YQIBztk6sqIRiw/Bh3Mw=="  
   And the decryption should bring "/0YQIBztk6sqIRiw/Bh3Mw==" back to "BAHLUL ENTE"  

 [INFO] ------------------------------------------------------------------------  
 [INFO] BUILD SUCCESS  
 [INFO] ------------------------------------------------------------------------  
 [INFO] Total time: 2.821s  
 [INFO] Finished at: Tue May 27 14:24:38 WIT 2014  
 [INFO] Final Memory: 8M/111M  
 [INFO] ------------------------------------------------------------------------  

yup, it succeed.

with cucumber, we (business analyst, developer or even partner) can easily check the test cases. this tool will show its power when working with complex business process. the partner and the analyst can be agree and deal in a same text file, which is feature file, that can be easily implemented by the developer in the unit test.

for better use, cucumber can be used in a continuous integration tool (like jenkins or bamboo or else) and run the test in schedule. any issue in the program can be easily check in the log file or in the ci's administration site.

just try it, have a nice code..
sample sourcecode: https://github.com/mazbergaz/sample_cucumber

No comments:

Post a Comment