Showing posts with label unit test. Show all posts
Showing posts with label unit test. Show all posts

Friday, December 15, 2017

Set-up Karma pada Grunt Task Runner


Pada kesempatan kali ini, saya akan membahas mengenai cara setting Karma Test Runner yang dapat di run melalui Grunt Javascript Task Runner. Sebelum melakukan hal tersebut ada baiknya kita mengetahui apa itu Karma dan Grunt. 


Singkatnya, Karma adalah sebuah Javascript Test Runner yang dibuat oleh Angular team, Karma digunakan pada Unit Testing sebuah aplikasi Javascript, khususnya Angular. Sedangkan Grunt adalah sebuah Javascript Task Runner dan sebagai build tools aplikasi javascript.

Yang perlu dipahami terlebih dahulu adalah file settingan pada Gruntfile.js. Gruntfile.js merupakan script yang ditulis untuk melakukan task-task pada project yang kita buat. Seperti menjalankan hot swap dengan melakukan watch terhadap segala perubahan file hingga task melakukan build otomatis dengan me-minified semua file pada project kita. Semua task pada Grunt bisa kita buat sebebas mungkin, namun dengan membutuhkan dependency lain untuk menjalankan suatu task yang dibuat.

Contoh task Grunt yang ada pada file Gruntfile.js:



Nah kemudian untuk membuat task Grunt yang menjalankan service Karma, pertama kita harus menginstall Karma server terlebih dahulu dengan command dibawah ini melalui npm:

npm install karma --save-dev.

Setelah karma terinstall, buatlah konfigurasi karma pada grunt.initConfig seperti dibawah ini:


perlu diketahui bahwasannya, atribute configFile merupakan sebuah file config karma yang bisa dibuat melalui command karma init pada folder test.

kurang lebih seperti ini gambaran file karma.conf.js:


Setelah itu pada list of files [], kita harus memasukan file-file yang dibutuhkan pada unit testing tersebut. mulai dari file angular, dependency-dependency lainnya dan jangan lupa test script yang akan masuk kedalam skenario pengujian kita.

untuk menjalankannya pada grunt kita hanya melakukan command dibawah ini:

grunt test

dan hasilnya seperti ini....


Oke cukup sekian penjelasannya mengenai Grunt dan Karma. Sampai jumpa di lain kesempatan dan 
Happy coding :D

References:










Tuesday, December 15, 2015

java class mocking dengan mockito

unit test sebaiknya dilakukan dengan menghilangkan dependency terhadap sistem lain. lalu bagaimana jika di dalam class/method yang hendak kita test mengandung komunikasi dengan sistem lain misalnya menembak API dari sistem lain?

kita bisa memalsukan method dari service yang menembak ke API sistem lain tersebut, sehingga seolah-olah sistem kita telah merequest dan menerima response. atau dengan kata lain, kita me-mock service tersebut, dan menentukan sendiri (berdasarkan asumsi dan/atau kondisi yang diinginkan dalam unit test tersebut) response yang diharapkan dari method tersebut.

mari kita coba dengan menggunakan library mockito. aplikasi menggunakan framework spring, unit test dengan junit.

misalkan kita punya class yang akan kita test:
 @Service  
 public class UseMeForTesting {  
        
      @Autowired  
      BidEngineRemoteService bidEngineRemoteService;  
        
      public String someTransaction() throws BidException{  
             
           String refNo = null;  
             
           BidEngineResponse response = bidEngineRemoteService.apiGet("/api/test");  
           if(response == null){  
                throw new BidException("no response.");  
           }else if(response.isError || response.getJsonResponse() == null){  
                throw new BidException("error response.");  
           }else if(response.getJsonResponse() != null){  
                refNo = response.getJsonResponse().get("refNo").toString();  
                refNo = refNo.replace("\"", "");  
           }  
             
           return refNo;  
             
      }  
        
      /**  
       * for mock injection purpose  
       * @param bidEngineRemoteService  
       */  
      public void setBidEngineRemoteService(BidEngineRemoteService bidEngineRemoteService){  
           this.bidEngineRemoteService = bidEngineRemoteService;  
      }  
        
 }  

di dalamnya memanggil service yang menembak api dari service lain.
 @Service  
 public class BidEngineRemoteService {  
   
   private static final Logger LOGGER = LoggerFactory.getLogger(BidEngineRemoteService.class);  
   
   @Autowired  
   private HttpRequest httpRequest;  
   
   @Value(value = "${bid.host}")  
   private String bidHost;  
   
   private final int MAX_REQUEST = 3;  
   
   public BidEngineResponse apiGet(String endpoint) {  
     return executeWithRetry(new ApiRequest() {  
       @Override  
       protected HttpRequest.HttpRequestResult doRequest() throws IOException {  
         return httpRequest.doGetRequestAndGetResult(createUrl(endpoint));  
       }  
     });  
   }  
   
   private String createUrl(String endpoint) {  
     if (!endpoint.substring(0, 1).equals("/")) {  
       endpoint = "/" + endpoint;  
     }  
     return bidHost + endpoint;  
   }  
   
   private BidEngineResponse executeWithRetry(ApiRequest apiRequest) {  
     int count = MAX_REQUEST;  
     while (count-- > 0) {  
       if (apiRequest.execute()) {  
         return apiRequest.getResult();  
       }  
     }  
     return null;  
   }  
   
   static abstract class ApiRequest {  
     private BidEngineResponse response;  
   
     protected abstract HttpRequest.HttpRequestResult doRequest() throws IOException;  
   
     public boolean execute() {  
       response = run();  
       return response != null;  
     }  
   
     private BidEngineResponse run() {  
       BidEngineResponse bidEngineResponse = null;  
       HttpRequest.HttpRequestResult result;  
   
       try {  
         result = doRequest();  
         bidEngineResponse = new BidEngineResponse(result);  
   
       } catch (IOException e) {  
         bidEngineResponse = new BidEngineResponse();  
         bidEngineResponse.isError = true;  
   
         if (e instanceof java.net.ConnectException) {  
           bidEngineResponse.setError("Could not connect to bid engine");  
         } else {  
           bidEngineResponse.setError(e.getMessage());  
         }  
   
         e.printStackTrace();  
       }  
   
       return bidEngineResponse;  
     }  
   
     public BidEngineResponse getResult() {  
       return response;  
     }  
   }  
   
   public static class BidEngineResponse extends HttpRequest.HttpRequestResult {  
   
     public static final String FIELD_ERROR = "Error";  
     private JsonObject root = null;  
   
     public BidEngineResponse() {  
       super();  
     }  
   
     public BidEngineResponse(HttpRequest.HttpRequestResult result) {  
       this.response = result.response;  
       this.isError = result.isError;  
   
       JsonElement jsonElement = result.response;  
       if (null != jsonElement && !jsonElement.isJsonNull()) {  
         if (jsonElement.isJsonObject()) {  
           root = jsonElement.getAsJsonObject();  
         } else {  
           root = new JsonObject();  
         }  
       } else {  
         root = new JsonObject();  
       }  
     }  
   
     public JsonObject getJsonResponse() {  
       return root;  
     }  
   
     public void setError(String message) {  
       root = null;  
       root = new JsonObject();  
       root.addProperty(FIELD_ERROR, message);  
     }  
   
     public String getError() {  
       return getValueFromErrorMap(FIELD_ERROR);  
     }  
   
     public String getValueFromErrorMap(String field) {  
       if (isError) {  
         JsonElement jsonElement = root.get(field);  
         if (null != jsonElement) {  
           return jsonElement.getAsString();  
         }  
       }  
       return null;  
     }  
       
     /**  
      * mock injection purpose  
      * @param isError  
      */  
     public void setError(boolean isError){  
          this.isError = isError;  
     }  
       
     /**  
      * mock injection purpose  
      * @param root  
      */  
     public void setRoot(JsonObject root){  
          this.root = root;  
     }  
   
   }  
        
 }  

untuk menghindari dependency, maka kita bisa membuat 'mock' untuk service di atas dengan menggunakan asumsi response yang diharapkan dari service tersebut. berikut contoh implementasi mocking dengan mockito pada jUnit test class.

 @RunWith(SpringJUnit4ClassRunner.class)  
 @ContextConfiguration(locations = {"/spring/applicationContext.xml"})  
 public class UseMeForTestingTest {  
        
      @Mock  
      BidEngineRemoteService bidEngineRemoteService;  
        
      @Autowired  
      UseMeForTesting useMeForTesting;  
        
      @Before  
      public void setUp() throws Exception {  
           MockitoAnnotations.initMocks(this);  
      }  
        
      private static final String REFNO = "test123";  
        
      @Test  
      public void someTransactionTest(){  
             
           JsonObject obj = new JsonObject();  
           obj.addProperty("refNo", REFNO);  
           obj.addProperty("message", "pesan");  
             
           BidEngineRemoteService.BidEngineResponse bidResponse = new BidEngineRemoteService.BidEngineResponse();  
           bidResponse.setError(false);  
           bidResponse.setRoot(obj);  
             
           Mockito.doReturn(bidResponse)  
                .when(bidEngineRemoteService)  
                .apiGet("/api/test");  
             
           useMeForTesting.setBidEngineRemoteService(bidEngineRemoteService);  
             
           try {  
                String result = useMeForTesting.someTransaction();  
                Assert.assertTrue(REFNO.equals(result));  
           } catch (BidException e) {  
                Assert.assertTrue(false);  
           }  
             
      }  
        
 }  

pada contoh di atas, kita mengasumsikan bidEngineRemoteService.apiGet("/api/test") akan meresponse dengan root object obj dan error=false. sehingga method UseMeForTesting.someTransaction() akan memberikan hasil refNo, sesuai dengan mock yang kita set.

silakan lihat http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html untuk contoh-contoh lainnya.

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