Purpose of Unit Testing
Whenever the topic of unit testing is raised.We will see lots of weird faces.Good number of developers will say why we need to write unit tests.Its waste of time.If any issues will come, we will debug it and resolve it.
But I feel that unit testing is important.Reasoning behind it is as follows:-
- Everyone feels that their implementation is fine.Various components in the code are loosely coupled and independent.However its during test case writing time that we can make out how good the code is.It provides an insight about the quality of code.
- Its rare that we write the perfect code.Any code goes through refactoring cycles.Having unit test comes handy.We can validate whether refactoring has not broke any existing workflow or not.
- Good unit test case is like a developer guide.Other developers can look at test case to figure out how they can integrate their code.
So Unit testing is like going to Gym.We know its good for us.But it just takes an extra effort to reach towards it. But just like it keeps you fit and healthy.It gives you confidence that your implementation is doing what it is supposed to do.
JUnit4
JUnit is the standard unit testing library for the Java. JUnit 4 simplifies testing by using annotation feature provided by Java 5 rather than relying on subclassing, reflection, and naming convention.
Annotations introduced in JUnit4 are as follows:-
- @Test
- @BeforeClass
- @AfterClass
- @Before
- @After
- @Parameters
@Test
@Test is a replacement of both TestCase class and convention "test" which we prefix to every test method.Any method which we want as test method should have this annotation.
@Before and @After
In case we want to execute something before and after the test case.We annotate methods with these annotations. So in case we have three test methods.Then methods annotated with @Before and @After will be invoked three times.
@BeforeClass and @AfterClass
In case we want to execute something only once for a test class. Then we annotate static methods with @BeforeClass and @AfterClass.So if your test class has five methods.Then these methods will be executed only once
@Parameters
Lets suppose you want to run a test for different inputs.We can specify @Parameters annotation for that.
Use Case of a banking system
To understand the concepts better,lets take a use case of a banking system.
Use case has following classes.These are:-
- BankAccount model class containing account number,contact number,address,name and others
- Bank class which contains list of bank accounts along with bank name ,address and others.
- IBankOperations interface containing general methods for deposit, withdrawl and others.
- BankOperationsImpl class implementing IBankOperations having bank as the reference.
Model Class for BankAccount containing account number,address and others.
package com.kunaal.model;
/**
* Model class for BankAccount
*
* @author ktrehan
*/
public class BankAccount {
// Variable for bank account number
private String acctNumber;
// Variable for account holder name
private String name;
// Variable for account holder address
private String address;
// Variable for account holder contact number
private String contactNumber;
// Variable for account balance
private Double currBalance;
/**
* Parameterized constructor
*
* @param acctNumber
* @param name
* @param address
* @param contactNumber
*/
public BankAccount(String acctNumber, String name, String address,
String contactNumber, Double currBalance) {
this.acctNumber = acctNumber;
this.name = name;
this.address = address;
this.contactNumber = contactNumber;
this.currBalance = currBalance;
}
/**
* @return the acctNumber
*/
public String getAcctNumber() {
return acctNumber;
}
/**
* @param acctNumber
* the acctNumber to set
*/
public void setAcctNumber(String acctNumber) {
this.acctNumber = acctNumber;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name
* the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the address
*/
public String getAddress() {
return address;
}
/**
* @param address
* the address to set
*/
public void setAddress(String address) {
this.address = address;
}
/**
* @return the contactNumber
*/
public String getContactNumber() {
return contactNumber;
}
/**
* @param contactNumber
* the contactNumber to set
*/
public void setContactNumber(String contactNumber) {
this.contactNumber = contactNumber;
}
/**
* @return the currBalance
*/
public Double getCurrBalance() {
return currBalance;
}
/**
* @param currBalance
* the currBalance to set
*/
public void setCurrBalance(Double currBalance) {
this.currBalance = currBalance;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "BankAccount [acctNumber=" + acctNumber + ", name=" + name
+ ", address=" + address + ", contactNumber=" + contactNumber
+ ", currBalance=" + currBalance + "]";
}
}
Bank.java
Model class for Bank containing list of bank accounts,bank address and others.
package com.kunaal.model;
import java.util.ArrayList;
import java.util.List;
/**
* Model class for Bank
*
* @author ktrehan
*/
public class Bank {
// Variable for bank name
private String name;
// Variable for bank address
private String address;
// Variable for bank contact number
private String contactNumber;
// Variable for list of bank accounts
private List<BankAccount> acctList = new ArrayList<BankAccount>();
/**
* Parameterized constructor
*
* @param name
* @param address
* @param contactNumber
* @param acctList
*/
public Bank(String name, String address, String contactNumber,
List<BankAccount> acctList) {
this.name = name;
this.address = address;
this.contactNumber = contactNumber;
this.acctList = acctList;
}
/**
* Parameterized constructor
*
* @param name
* @param address
* @param contactNumber
*/
public Bank(String name, String address, String contactNumber) {
this.name = name;
this.address = address;
this.contactNumber = contactNumber;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name
* the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the address
*/
public String getAddress() {
return address;
}
/**
* @param address
* the address to set
*/
public void setAddress(String address) {
this.address = address;
}
/**
* @return the contactNumber
*/
public String getContactNumber() {
return contactNumber;
}
/**
* @param contactNumber
* the contactNumber to set
*/
public void setContactNumber(String contactNumber) {
this.contactNumber = contactNumber;
}
/**
* @return the acctList
*/
public List<BankAccount> getAcctList() {
return acctList;
}
/**
* @param acctList
* the acctList to set
*/
public void setAcctList(List<BankAccount> acctList) {
this.acctList = acctList;
}
}
IBankOperations.java
Interface for banking operations
package com.kunaal.service;
import com.kunaal.model.BankAccount;
/**
* @author ktrehan
*
*/
public interface IBankOperations {
/**
* Method for depositing money in a particular account
*
* @param acctNumber
* @param amount
*/
void deposit(String acctNumber, Double amount);
/**
* Method for withdrawing money from a particular account
*
* @param acctNumber
* @param amount
*/
void withdrawl(String acctNumber, Double amount);
/**
* Method for fetching account details
*
* @param acctNumber
* @return
*/
BankAccount acctDetails(String acctNumber);
/**
* Method for updating the address
*
* @param acctNumber
* @param newAddress
*/
void updateAddress(String acctNumber, String newAddress);
/**
* Method for updating contact number
*
* @param acctNumber
* @param contactNumber
*/
void updateContactNum(String acctNumber, String contactNumber);
}
BankOperationsImpl.java
Implementation of IBankOperations interface
package com.kunaal.service;
import java.util.List;
import com.kunaal.model.Bank;
import com.kunaal.model.BankAccount;
/**
* Implementation for IBankOperations interface
*
* @author ktrehan
*/
public class BankOperationsImpl implements IBankOperations {
// Variable for Bank
private Bank bank;
/**
* Parameterized constructor
*
* @param bank
*/
public BankOperationsImpl(Bank bank) {
this.bank = bank;
}
/**
* Method for depositing money in a particular account
*
* @param acctNumber
* @param amount
*/
public void deposit(String acctNumber, Double amount) {
List<BankAccount> acctList = bank.getAcctList();
boolean acctMatched = false;
for (BankAccount bankAcct : acctList) {
String acctVal = bankAcct.getAcctNumber();
if (acctVal != null && acctVal.equals(acctNumber)) {
Double currBalance = bankAcct.getCurrBalance();
bankAcct.setCurrBalance(currBalance + amount);
acctMatched = true;
break;
}
}
if (!acctMatched)
throw new RuntimeException("No acct matched");
}
/**
* Method for withdrawing money from a particular account
*
* @param acctNumber
* @param amount
*/
public void withdrawl(String acctNumber, Double amount) {
List<BankAccount> acctList = bank.getAcctList();
boolean acctMatched = false;
for (BankAccount bankAcct : acctList) {
String acctVal = bankAcct.getAcctNumber();
if (acctVal != null && acctVal.equals(acctNumber)) {
Double currBalance = bankAcct.getCurrBalance();
bankAcct.setCurrBalance(currBalance - amount);
acctMatched = true;
break;
}
}
if (!acctMatched)
throw new RuntimeException("No acct matched");
}
/**
* Method for fetching account details
*
* @param acctId
* @return
*/
public BankAccount acctDetails(String acctNumber) {
List<BankAccount> acctList = bank.getAcctList();
boolean acctMatched = false;
BankAccount returnVal = null;
for (BankAccount bankAcct : acctList) {
String acctVal = bankAcct.getAcctNumber();
if (acctVal != null && acctVal.equals(acctNumber)) {
acctMatched = true;
returnVal = bankAcct;
break;
}
}
if (!acctMatched)
throw new RuntimeException("No acct matched");
return returnVal;
}
/**
* Method for updating the address
*
* @param acctId
* @param newAddress
*/
public void updateAddress(String acctNumber, String newAddress) {
List<BankAccount> acctList = bank.getAcctList();
boolean acctMatched = false;
for (BankAccount bankAcct : acctList) {
String acctVal = bankAcct.getAcctNumber();
if (acctVal != null && acctVal.equals(acctNumber)) {
acctMatched = true;
bankAcct.setAddress(newAddress);
break;
}
}
if (!acctMatched)
throw new RuntimeException("No acct matched");
}
/**
* Method for updating contact number
*
* @param acctId
* @param contactNumber
*/
public void updateContactNum(String acctNumber, String contactNumber) {
List<BankAccount> acctList = bank.getAcctList();
boolean acctMatched = false;
for (BankAccount bankAcct : acctList) {
String acctVal = bankAcct.getAcctNumber();
if (acctVal != null && acctVal.equals(acctNumber)) {
bankAcct.setContactNumber(contactNumber);
acctMatched = true;
break;
}
}
if (!acctMatched)
throw new RuntimeException("No acct matched");
}
/**
* @return the bank
*/
public Bank getBank() {
return bank;
}
/**
* @param bank
* the bank to set
*/
public void setBank(Bank bank) {
this.bank = bank;
}
}
Test Classes
Now lets test the methods for BankOperationsImpl.
Test case showing usage of
- @Test
- @BeforeClass
- @AfterClass
package com.kunaal.service;
import static org.junit.Assert.assertEquals;
import java.util.List;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import com.kunaal.model.Bank;
import com.kunaal.model.BankAccount;
/**
* Test class showing usage of @BeforeClass ,@AfterClass Besides this we are
* testing implementations of IBankOperations methods.
*
* @author ktrehan
*/
public class BankOpTest {
// Static variable for Bank
private static Bank bank;
// Static variable for IBankOperations
private static IBankOperations bankOperations;
/**
* Static method for constructing different bank accounts and wiring the
* same with the bank
*/
@BeforeClass
public static void setup() {
System.out.println("Static setup invoked");
BankAccount bankAcct1 = new BankAccount("AcctNum-1", "Mr. A1",
"Addr-1", "Contact-1", new Double("10000"));
BankAccount bankAcct2 = new BankAccount("AcctNum-2", "Mr. A2",
"Addr-2", "Contact-2", new Double("20000"));
BankAccount bankAcct3 = new BankAccount("AcctNum-3", "Mr. A3",
"Addr-3", "Contact-3", new Double("30000"));
bank = new Bank("ICICI", "Bangalore", "91-80-4189000");
List<BankAccount> acctList = bank.getAcctList();
acctList.add(bankAcct1);
acctList.add(bankAcct2);
acctList.add(bankAcct3);
bankOperations = new BankOperationsImpl(bank);
}
/**
* Static method for tear down
*/
@AfterClass
public static void teardown() {
System.out.println("Static teardown invoked");
}
/**
* Test case for deposit operation
*/
@Test
public void deposit() {
System.out.println("1. DEPOSIT TEST CASE INVOKED");
bankOperations.deposit("AcctNum-1", new Double("123"));
BankAccount acctDetails = bankOperations
.acctDetails("AcctNum-1");
assertEquals(acctDetails.getCurrBalance(), new Double("10123"));
}
/**
* Test case for withdraw operation
*/
@Test
public void withdraw() {
System.out.println("2. WITHDRAW TEST CASE INVOKED");
bankOperations.withdrawl("AcctNum-2", new Double("100"));
BankAccount acctDetails = bankOperations
.acctDetails("AcctNum-2");
assertEquals(acctDetails.getCurrBalance(), new Double("19900"));
}
/**
* Test case for updating address
*/
@Test
public void updateAddr() {
System.out.println("3. UPDATE ADDRESS TEST CASE INVOKED");
bankOperations.updateAddress("AcctNum-3", "Delhi");
BankAccount acctDetails = bankOperations
.acctDetails("AcctNum-3");
assertEquals(acctDetails.getAddress(), "Delhi");
}
/**
* Test case for updating phone
*/
@Test
public void updatePhone() {
System.out.println("4. UPDATE PHONE TEST CASE INVOKED");
bankOperations.updateContactNum("AcctNum-3", "NewContactNumber");
BankAccount acctDetails = bankOperations
.acctDetails("AcctNum-3");
assertEquals(acctDetails.getContactNumber(), "NewContactNumber");
}
}
Output of running the test case
It shows that @BeforeClass and @AfterClass is executed once for TestClass.
Static setup invoked
1. DEPOSIT TEST CASE INVOKED
2. WITHDRAW TEST CASE INVOKED
3. UPDATE ADDRESS TEST CASE INVOKED
4. UPDATE PHONE TEST CASE INVOKED
Static teardown invoked
Test case showing usage of
- @Before
- @After
- timeout parameter in @Test
- exception parameter in @Test
package com.kunaal.service;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.kunaal.model.Bank;
import com.kunaal.model.BankAccount;
/**
* Test case showing the usage of @After @Before along with timeout and
* exception check
*
* @author ktrehan
*/
public class ScenarioTest {
// Variable for bank
private Bank bank;
// Variable for IBankOperations
private IBankOperations bankOp;
/**
* Non static method for creating different bank accounts and wiring the
* same.
*/
@Before
public void init() {
System.out.println("Non Static setup invoked");
BankAccount bankAcct1 = new BankAccount("AcctNum-1", "Mr. A1",
"Addr-1", "Contact-1", new Double("10000"));
BankAccount bankAcct2 = new BankAccount("AcctNum-2", "Mr. A2",
"Addr-2", "Contact-2", new Double("20000"));
BankAccount bankAcct3 = new BankAccount("AcctNum-3", "Mr. A3",
"Addr-3", "Contact-3", new Double("30000"));
bank = new Bank("ICICI", "Bangalore", "91-80-4189000");
List<BankAccount> acctList = bank.getAcctList();
acctList.add(bankAcct1);
acctList.add(bankAcct2);
acctList.add(bankAcct3);
bankOp = new BankOperationsImpl(bank);
}
/**
* Non static method for tear down
*/
@After
public void tearDown() {
bankOp = null;
bank = null;
System.out.println(" Non static tear down invoked");
}
/**
* Test case for getting account details
*/
@Test
public void getAcctDetails() {
System.out.println("1. Acct details for AcctNum-1 invoked");
BankAccount acctDetails = bankOp.acctDetails("AcctNum-1");
assertEquals(acctDetails.getCurrBalance(), new Double("10000"));
}
/**
* Test case for getting account details
*/
@Test
public void getAcctDetails1() {
System.out.println("2. Acct details for AcctNum-2 invoked");
BankAccount acctDetails = bankOp.acctDetails("AcctNum-2");
assertEquals(acctDetails.getCurrBalance(), new Double("20000"));
}
/**
* Test case for timeout Here we have given timeout of 2000ms and in the
* body we have given sleep of 3000ms So testcase will fail
*/
@Test(timeout = 2000)
public void timeOutTest() {
System.out.println("3. Test case for timeout invoked");
try {
Thread.currentThread().sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
BankAccount acctDetails = bankOp.acctDetails("AcctNum-2");
assertEquals(acctDetails.getCurrBalance(), new Double("20000"));
}
/**
* Test case for exception verification
*/
@Test(expected = RuntimeException.class)
public void exceptionTest() {
System.out.println("4. Test case for exception handling");
BankAccount acctDetails = bankOp.acctDetails("123");
fail();
}
}
Output of running this test case.
It shows that @Before and @After is invoked for every test case.
Besides this timeout exception happened and test case with timeout failed.
Non Static setup invoked
1. Acct details for AcctNum-1 invoked
Non static tear down invoked
Non Static setup invoked
2. Acct details for AcctNum-2 invoked
Non static tear down invoked
Non Static setup invoked
3. Test case for timeout invoked
Non static tear down invoked
java.lang.Exception: test timed out after 2000 milliseconds
at java.lang.Thread.sleep(Native Method)
at com.kunaal.service.ScenarioTest.timeOutTest(ScenarioTest.java:91)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.internal.runners.statements.FailOnTimeout$1.run(FailOnTimeout.java:28)
Non Static setup invoked
4. Test case for exception handling
Non static tear down invoked
TestCase showing usage of
- @Parameters
- @RunWith(value = Parameterized.class)
So if you've ever found yourself writing a series of tests which differ only in their inputs and expected results, you've probably realized that the sensible thing to do would be to abstract your tests into a single test that can be run against a varying set of data. JUnit 4 allows you to do this with either theories or parameterized tests
Structure of a parameterized test class
To mark a test class as a parameterized test, you must first annotate it with @RunWith(Parameterized.class). The class must then provide at least three entities:
To mark a test class as a parameterized test, you must first annotate it with @RunWith(Parameterized.class). The class must then provide at least three entities:
- Static method that generates and returns test data,
- Single constructor that stores the test data
- Test
The method that generates test data must be annotated with @Parameters, and it must return a Collection of Arrays. Each array represents the data to be used in a particular test run. The number of elements in each array must correspond to the number of parameters in the class's constructor, because each array element will be passed to the constructor, one at a time as the class is instantiated over and over.
The constructor is simply expected to store each data set in the class's fields, where they can be accessed by the test methods. Note that only a single constructor may be provided. This means that each array provided by the data-generating method must be the same size, and you might have to pad your data sets with nulls if you don't always need a particular value.
Let's put this together. When the test runner is invoked, the data-generating method will be executed, and it will return a Collection of Arrays, where each array is a set of test data. The test runner will then instantiate the class and pass the first set of test data to the constructor. The constructor will store the data in its fields. Then each test method will be executed, and each test method will have access to that first set of test data. After each test method has executed, the object will be instantiated again, this time using the second element in the Collection of Arrays, and so on.
Use case of CurrencyConverter
CurrencyConverter converts amount into INR.It has one public method valInINR(...,..) for the same.
CurrencyConverter.java
This class converts the amount into INR.
package com.kunaal.converter;
import java.util.HashMap;
import java.util.Map;
/**
* Java class for doing currency conversion
*
* @author ktrehan
*/
public class CurrencyConverter {
/**
* Map for holding currency and conversion factor
*/
private Map<String, Double> conversionMap = new HashMap<String, Double>();
/**
* Constructor for currency conversion Here we map currency code and
* conversion amount for INR
*/
public CurrencyConverter() {
conversionMap.put("USD", new Double(53));
conversionMap.put("UK", new Double(65));
conversionMap.put("AUD", new Double(35));
conversionMap.put("CAD", new Double(45));
}
/**
* Method to get value in INR for a particular amount in different
* currency
*
* @param currCode
* @param amount
* @return
*/
public Double valInINR(String currCode, Double amount) {
return conversionMap.get(currCode) * amount;
}
}
Lets write the parameterized test case where we convert 1000 USD/UK/AUD/CAD into Indian Rupees.We will create the test input in one static block,which will be used by the constructor and test method is invoked.
Test case showing usage of
- @Parameters
package com.kunaal.service;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import com.kunaal.converter.CurrencyConverter;
/**
* @author ktrehan
*
*/
@RunWith(value = Parameterized.class)
public class ParameterizedTest {
// Input parameter for currency code
private String currCode;
// Input parameter for amount
private Double inputAmt;
// Output parameter for converted amount
private Double outAmt;
// Static variable for Currency Converter
private static CurrencyConverter currConverter;
/**
* Static method for test input and output
*
* @return
*/
@Parameters
public static Collection<Object[]> paramData() {
List<Object[]> dataList = new ArrayList<Object[]>();
dataList.add(new Object[] { "USD", new Double(1000),
new Double(53000) });
dataList.add(new Object[] { "UK", new Double(1000),
new Double(65000) });
dataList.add(new Object[] { "AUD", new Double(1000),
new Double(35000) });
dataList.add(new Object[] { "CAD", new Double(1000),
new Double(45000) });
return dataList;
}
/**
* Constructor taking input parameters and corresponding output
*
* @param currCode
* @param inptAmt
* @param outAmt
*/
public ParameterizedTest(String currCode, Double inptAmt, Double outAmt) {
this.currCode = currCode;
this.inputAmt = inptAmt;
this.outAmt = outAmt;
}
/**
* Static @BeforeClass method
*/
@BeforeClass
public static void setup() {
currConverter = new CurrencyConverter();
}
/**
* Test Case for checking conversion amount
*/
@Test
public void valInINR() {
System.out.println("Currency code used -" + this.currCode
+ " ,amount for conversion-" + this.inputAmt);
assertEquals(currConverter.valInINR(this.currCode,
this.inputAmt), this.outAmt);
System.out.println("Output amount-" + this.outAmt);
}
}
Output of running this test case is as follows:-
Currency code used -USD ,amount for conversion-1000.0
Output amount-53000.0
Currency code used -UK ,amount for conversion-1000.0
Output amount-65000.0
Currency code used -AUD ,amount for conversion-1000.0
Output amount-35000.0
Currency code used -CAD ,amount for conversion-1000.0
Output amount-45000.0
So this completes some of the important features of JUnit4.
Obstacles in the path of Unit testing
Most of classes in production code has dependencies on other classes or modules.
When writing a unit test one of the challenges is how to code around the dependencies of the object of his test. In other words – how to isolate the code under test from external dependencies.
Imagine a use case work where we need to interact with a web service and apply some computation logic on the response received.
Sometimes in web application we fetch data from HTTPSession and work on it.So how we should get HTTPSession data during unit test phase.
Sometimes method under test needs access to database.So we should provide database access.
So to resolve all such issues, we have fake objects like Mocks and Stubs.
Mocks and Stubs
In order to get rid of dependent objects during unit testing.We need fake objects.Mocks and Stubs are two ways to achieve the same.
Stubs
Stubs provide pre -programmed answers to method invocation.Testing methodology followed by Stubs is 'State Based'
Mocks
Mocks mimics the real objects in controlled defined way.Testing methodology followed by Mocks is 'Interaction Based'.
In order to understand the difference between Stub and Mock ,lets take the use case of NetWorthCalculator.
Use case has following classes.These are:-
- IStockFeeder-This interface contains method for finding net stock value based on demat account number
- ILoanFeeder-This interface contains method to find principal amount pending.
- IMFFeeder-This interface contains method for finding net value based on mutual fund account number.
- NetWorthCalculator-Implementation class having MFFeeder,LoanFeeder and StockFeeder as attributes.netWorth() method calculates net worth based on some rules.
package com.kunaal.feeder;
/**
* Interface for Loan information
*
* @author ktrehan
*/
public interface ILoanFeeder {
/**
* Method for finding pending principal amount based on laon account
* number.
*
* @param loanActNum
* @return
*/
Double pendingPrinAmount(String loanActNum);
}
IMFFeeder.java
package com.kunaal.feeder;
/**
* Interface for finding Mutual Fund information
*
* @author ktrehan
*/
public interface IMFFeeder {
/**
* Method for finding net Mutual fund value based on account number
*
* @param mfAcctNum
* @return
*/
Double netMFValue(String mfAcctNum);
}
IStockFeeder.java
package com.kunaal.feeder;
/**
* Interface for finding stock information.
*
* @author ktrehan
*/
public interface IStockFeeder {
/**
* Method for finding net stock value based on demat account number.
*
* @param dematAcctNum
* @return
*/
Double netStockValue(String dematAcctNum);
}
NetWorthCalculator.java
package com.kunaal;
import com.kunaal.feeder.ILoanFeeder;
import com.kunaal.feeder.IMFFeeder;
import com.kunaal.feeder.IStockFeeder;
/**
* Networth calculator class.
*
* @author ktrehan
*/
public class NetWorthCalculator {
// Variable for IMFFeeder
private IMFFeeder mfFeeder;
// Variable for IStockFeeder
private IStockFeeder stockFeeder;
// Variable for ILoanFeeder
private ILoanFeeder loanFeeder;
// Variable for cash amount
private Double cashAmt;
// Variable for fixed asset val
private Double fixedAssetVal;
// Boolean variable stating whether any loans are there or not
private boolean anyLoans;
// Variable for Mutual Fund Account Number
private String mfAcctNum;
// Variable for Demat Account Number
private String dematAcctNum;
// Variable for Loan Account Number
private String loanAcctNum;
/**
* Parameterized constructor
*
* @param mfFeeder
* @param stockFeeder
* @param loanFeeder
* @param cashAmt
* @param fixedAssetVal
* @param anyLoans
* @param mfAcctNum
* @param dematAcctNum
* @param loanAcctNum
*/
public NetWorthCalculator(IMFFeeder mfFeeder, IStockFeeder stockFeeder,
ILoanFeeder loanFeeder, Double cashAmt,
Double fixedAssetVal, boolean anyLoans,
String mfAcctNum, String dematAcctNum,
String loanAcctNum) {
this.mfFeeder = mfFeeder;
this.stockFeeder = stockFeeder;
this.loanFeeder = loanFeeder;
this.cashAmt = cashAmt;
this.fixedAssetVal = fixedAssetVal;
this.anyLoans = anyLoans;
this.mfAcctNum = mfAcctNum;
this.dematAcctNum = dematAcctNum;
this.loanAcctNum = loanAcctNum;
}
/**
* Method having logic for finding total worth
*
* @return
*/
public Double netWorth() {
Double netWorthVal = new Double(0);
netWorthVal = cashAmt + 0.85 * fixedAssetVal + 0.9
* mfFeeder.netMFValue(mfAcctNum) + 0.7
* stockFeeder.netStockValue(dematAcctNum);
if (anyLoans) {
netWorthVal = netWorthVal
- (1.3 * loanFeeder
.pendingPrinAmount(loanAcctNum));
}
return netWorthVal;
}
}
Testing NetWorthCalculator using Stub Methodology
Here we have write following stubs with hard coded return values. These are:-
- StubMFFeeder
- StubLoanFeeder
- StubStockFeeder
These stubs are then wired in NetWorthCalculatorTest.While creating object of NetWorthCalculator,we are passing false to 'anyLoans'.So even though we have created StubLoanFeeder,method for finding pending principal amount is never invoked.However with the use of stub methodology this can't be verified.
package com.kunaal.stub;
import com.kunaal.feeder.IMFFeeder;
/**
* Stub for MF feeder
*
* @author ktrehan
*/
public class StubMFFeeder implements IMFFeeder {
/**
* Hard coded value for method implementation
*/
public Double netMFValue(String mfAcctNum) {
return new Double(100000);
}
}
StubLoanFeeder.java
package com.kunaal.stub;
import com.kunaal.feeder.ILoanFeeder;
/**
* Stub for Loan Feeder
*
* @author ktrehan
*/
public class StubLoanFeeder implements ILoanFeeder {
/**
* Hard coded value for method implementation
*/
public Double pendingPrinAmount(String loanActNum) {
return new Double(50000);
}
}
StubStockFeeder.java
package com.kunaal.stub;
import com.kunaal.feeder.IStockFeeder;
/**
* Stub implementation for Stock Feeder
*
* @author ktrehan
*/
public class StubStockFeeder implements IStockFeeder {
/**
* Hard coded value for method implementation.
*/
public Double netStockValue(String dematAcctNum) {
return new Double(20000);
}
}
NetWorthCalcTest.java
package com.kunaal.stub;
import static org.junit.Assert.assertEquals;
import org.junit.BeforeClass;
import org.junit.Test;
import com.kunaal.NetWorthCalculator;
import com.kunaal.feeder.ILoanFeeder;
import com.kunaal.feeder.IMFFeeder;
import com.kunaal.feeder.IStockFeeder;
/**
* Stub approach for testing Net Worth Calculator.
*
* @author ktrehan
*/
public class NetWorthCalcTest {
// Variable for IMFFeeder
private static IMFFeeder mfFeeder;
// Variable for IStockFeeder
private static IStockFeeder stockFeeder;
// Variable for ILoanFeeder
private static ILoanFeeder loanFeeder;
// Static variable for NetWorthCalculator
private static NetWorthCalculator netWorthCalc;
/**
* Static method for object creation.
*/
@BeforeClass
public static void setup() {
mfFeeder = new StubMFFeeder();
stockFeeder = new StubStockFeeder();
loanFeeder = new StubLoanFeeder();
netWorthCalc = new NetWorthCalculator(mfFeeder, stockFeeder,
loanFeeder, new Double(25000), new Double(
1000000), false, "MF-1",
"Demat-1", null);
}
/**
* Test method for finding networth.
*/
@Test
public void networth() {
assertEquals(netWorthCalc.netWorth(), new Double(979000));
System.out.println("Test case passed");
}
}
Output
Test cases will run successfully with console output of 'Test case passed'.
Testing NetWorthCalculator using Mock Methodology
Here we are creating only one class and using Mockito library for creating mocks of dependencies.
package com.kunaal.mock;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.junit.BeforeClass;
import org.junit.Test;
import com.kunaal.NetWorthCalculator;
import com.kunaal.feeder.ILoanFeeder;
import com.kunaal.feeder.IMFFeeder;
import com.kunaal.feeder.IStockFeeder;
/**
* Mock approach for testing NetWorthCalculator
*
* @author ktrehan
*/
public class NetWorthCalcTest {
// Variable for IMFFeeder
private static IMFFeeder mfFeeder;
// Variable for IStockFeeder
private static IStockFeeder stockFeeder;
// Variable for ILoanFeeder
private static ILoanFeeder loanFeeder;
// Variable for NetWorthCalculator
private static NetWorthCalculator netWorthCalc;
/**
* Static method for creation of mocks and wiring the same
*/
@BeforeClass
public static void setup() {
// Creating mock objects of IMFFeeder,IStockFeeder,ILoanFeeder
mfFeeder = mock(IMFFeeder.class);
stockFeeder = mock(IStockFeeder.class);
loanFeeder = mock(ILoanFeeder.class);
netWorthCalc = new NetWorthCalculator(mfFeeder, stockFeeder,
loanFeeder, new Double(25000), new Double(
1000000), false, "MF-1",
"Demat-1", null);
}
/**
* Testing the netWorth()
*/
@Test
public void networth() {
// stubbing or creating the expectations
when(mfFeeder.netMFValue(anyString())).thenReturn(
new Double(100000));
when(stockFeeder.netStockValue(anyString())).thenReturn(
new Double(20000));
when(loanFeeder.pendingPrinAmount(anyString())).thenReturn(
new Double(50000));
// Testing the netWorth() method implementation
assertEquals(netWorthCalc.netWorth(), new Double(979000));
// verification routines for method invocation
verify(mfFeeder, times(1)).netMFValue(anyString());
verify(stockFeeder, times(1)).netStockValue(anyString());
verify(loanFeeder, times(1)).pendingPrinAmount(anyString());
}
}
Output
Wanted but not invoked:
iLoanFeeder.pendingPrinAmount(<any>);
-> at com.kunaal.mock.NetWorthCalcTest.networth(NetWorthCalcTest.java:72)
Actually, there were zero interactions with this mock.
at com.kunaal.mock.NetWorthCalcTest.networth(NetWorthCalcTest.java:72)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
So you can see using mock approach has following advantages over stub approach.These are:-
- We donot need to hand code the stub classes
- Creation of mock is easy.
- Method expectations can be set easily
- Besides state ,we can also verify the interactions.
Mockito Features
- Mockito can mock both interface and class.
Mockito can mock both interfaces and class.It uses proxy to achieve this.
In this example,we will mock List(An interface) and Exception (A class)
ListTest.java
package com.kunaal.intClass; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.List; import org.junit.Test; /** * This test class illustrates that mockito can mock both class and interfaces * It uses both java dynamic proxy and cglib and asm handlers * * @author ktrehan */ public class ListTest { /** * Mockito in picture for handling interfaces */ @Test public void interfaceTest() { // Mock object creation List<String> mockList = mock(List.class); // method invocation on mocked object mockList.add("USA"); mockList.add("UK"); mockList.add("India"); // Verifying the interaction verify(mockList, times(3)).add(anyString()); } /** * Mockito in picture for handling classes */ @Test public void classTest() { // Mock object creation Exception mockException = mock(Exception.class); String output = "Test exception message"; // Setting expectation when(mockException.getMessage()).thenReturn(output); assertEquals(output, mockException.getMessage()); // verify the interaction verify(mockException, times(1)).getMessage(); } }
- Easy way of matching method argument.
A method invocation can happen for any value.Mockito has a feature to generalize this.Instead of hard coding we can write any...() tags for argument matching.
Here we have a TaxCalculator which provides tax percentage depending upon the country and
salary amount
TaxCalculator.javapackage com.kunaal.matchers; /** * Utility class for finding tax percentage * * @author ktrehan */ public class TaxCalc { /** * Utility method for finding tax percentage slab * * @param country * @param annIncome * @return */ public double getTaxBracket(String country, Double annIncome) { double taxPercentage = 0d; if (country.equalsIgnoreCase("USA")) { if (annIncome < 1000000) taxPercentage = 10.5; else taxPercentage = 15; } else if (country.equalsIgnoreCase("UK")) { if (annIncome < 50000) taxPercentage = 12; else taxPercentage = 19; } else { taxPercentage = 13; } return taxPercentage; } }
Test class showing usage of argument matchers(anyString(),anyDouble(),....)
MatcherTest.java
package com.kunaal.matchers; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.anyDouble; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.junit.Test; /** * This test class illustrates the argument matcher feature present in Mockito * * @author ktrehan */ public class MatcherTest { /** * Mockito feature showing argument matcher like * anyString(),anyDouble()..... */ @Test public void multiArgTest() { TaxCalc mockObj = mock(TaxCalc.class); // Setting the expectation when(mockObj.getTaxBracket(anyString(), anyDouble())) .thenReturn(24d); double taxBracket = mockObj.getTaxBracket("USA", 12345678d); // assertion assertEquals(taxBracket, 24d, 0.0d); // verifications verify(mockObj, times(1)).getTaxBracket(anyString(), anyDouble()); } /** * Mockito feature showing argument matcher like * anyString(),anyDouble()..... along with different results for * multiple invocations. */ @Test public void multiArgTest1() { TaxCalc mockObj = mock(TaxCalc.class); // Setting the expectation when(mockObj.getTaxBracket(anyString(), anyDouble())) .thenReturn(24d).thenReturn(50d); double taxBracket = mockObj.getTaxBracket("USA", 12345678d); // assertion assertEquals(taxBracket, 24d, 0.0d); taxBracket = mockObj.getTaxBracket("UK", 123334455d); // assertion assertEquals(taxBracket, 50d, 0.0d); // verification verify(mockObj, times(2)).getTaxBracket(anyString(), anyDouble()); } }
- Verify number of method invocations and invocation order.
Mockito has a feature for verifying how many times a particular method is invoked
and in which order method invocations on particular object has happened.
InvocationTest.javapackage com.kunaal.verify; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import java.util.List; import org.junit.Test; import org.mockito.InOrder; /** * This test class illustrates the usage of verify and verification order * * @author ktrehan */ public class InvocationTest { /** * Mockito illustrating number of times a particular method is invoked */ @Test public void addOperation() { // Mock List<String> mockList = mock(List.class); mockList.add("A"); mockList.add("B"); mockList.add("C"); mockList.add("D"); // Verifying number of invocations for add(..) verify(mockList, times(4)).add(anyString()); // Verifying number of invocations for clear(...) verify(mockList, never()).clear(); } /** * Mockito in picture showing order in which methods are executed */ @Test public void invocationOrder() { // Mock List<String> mockList = mock(List.class); InOrder inorder = inOrder(mockList); mockList.add("A"); mockList.add("B"); mockList.get(0); mockList.clear(); // Invocation order verification inorder.verify(mockList).clear(); inorder.verify(mockList).add("A"); inorder.verify(mockList).add("B"); inorder.verify(mockList).get(0); inorder.verify(mockList).clear(); } }
Here addOperation() shows how many times add() method is invoked and invocationOrder() shows the order in which methods are invoked.
Output
org.mockito.exceptions.verification.VerificationInOrderFailure: Verification in order failure Wanted but not invoked: list.add("A"); -> at com.kunaal.verify.InvocationTest.invocationOrder(InvocationTest.java:55) Wanted anywhere AFTER following interaction: list.clear(); -> at com.kunaal.verify.InvocationTest.invocationOrder(InvocationTest.java:52) at com.kunaal.verify.InvocationTest.invocationOrder(InvocationTest.java:55) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:601) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184) at org.junit.runners.ParentRunner.run(ParentRunner.java:236) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
- Handling of generics Mockito handle Generics in different ways.One is by casting and another is by using annotations provided by Mockito.
To understand it better ,lets take above use case where Globe class contains a map containing map for currency and country details.We have following two generic variables.These are:-
-private Map<String, String> currMap; [Variable for currency information corresponding to country code]
-private Map<String, CtryDetails> ctryMap; [Variable for country details corresponding to country code]
Globe.java
package com.kunaal.generics; import java.util.List; import java.util.Map; /** * Class having composition of * -Country Details Map * -Currency Map * * @author ktrehan */ public class Globe { // Variable for capturing currency map private Map<String, String> currMap; // Variable for capturing country details private Map<String, CtryDetails> ctryMap; /** * Default constructor */ public Globe() { } /** * Method for finding country information. * * @param countryName * @return */ public String ctryType(String countryName) { CtryDetails ctryDetails = ctryMap.get(countryName); Long population = null; if (ctryDetails != null) { population = ctryDetails.getPopulation(); if (population > 100000000) return "Heavily Populated"; else return "Not heavily populated"; } else { return "Country Info not present"; } } /** * Method for finding currency of the country. * * @param countryName * @return */ public String getCurrency(String countryName) { String currency = currMap.get(countryName); if (currency != null) return currency; else return "Currency Information not avaialable"; } /** * Method for finding language details of the country * * @param countryName * @return */ public String langType(String countryName) { CtryDetails ctryDetails = ctryMap.get(countryName); List<String> langList = ctryDetails.getLangList(); if (langList != null) { if (langList.size() > 1) return "Multi-Lingual"; else return "One common language"; } else { return "Language data not found"; } } /** * @return the currMap */ public Map<String, String> getCurrMap() { return currMap; } /** * @param currMap * the currMap to set */ public void setCurrMap(Map<String, String> currMap) { this.currMap = currMap; } /** * @return the ctryMap */ public Map<String, CtryDetails> getCtryMap() { return ctryMap; } /** * @param ctryMap * the ctryMap to set */ public void setCtryMap(Map<String, CtryDetails> ctryMap) { this.ctryMap = ctryMap; } }
CtryDetails.java
package com.kunaal.generics; import java.util.ArrayList; import java.util.List; /** * Model class for Country Details * * @author ktrehan */ public class CtryDetails { // Variable for poulation private Long population = 0l; // Variable for list of languages private List<String> langList = new ArrayList<String>(); /** * @return the population */ public Long getPopulation() { return population; } /** * @param population * the population to set */ public void setPopulation(Long population) { this.population = population; } /** * @return the langList */ public List<String> getLangList() { return langList; } /** * @param langList * the langList to set */ public void setLangList(List<String> langList) { this.langList = langList; } }
First version of test case where we use casting for generics
GlobeTest.java
package com.kunaal.generics; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Map; import org.junit.BeforeClass; import org.junit.Test; /** * Test case for Globe using casting approach * * @author ktrehan */ public class GlobeTest { // Static variable for Globe private static Globe globeVar; // Static variable for currency map private static Map<String, String> currMap; // Static variable for country map private static Map<String, CtryDetails> ctryMap; /** * Static initializer */ @BeforeClass public static void init() { currMap = (Map<String, String>) mock(Map.class); ctryMap = (Map<String, CtryDetails>) mock(Map.class); globeVar = new Globe(); globeVar.setCtryMap(ctryMap); globeVar.setCurrMap(currMap); } /** * Test case for ctryMap */ @Test public void ctryType() { CtryDetails ctryDetails = new CtryDetails(); ctryDetails.setPopulation(100000l); when(ctryMap.get(anyString())).thenReturn(ctryDetails); assertEquals(globeVar.ctryType("ABCD"), "Not heavily populated"); } /** * Test case for currMap */ @Test public void getCurrency() { when(currMap.get(anyString())).thenReturn("USD"); assertEquals(globeVar.getCurrency("ABCD"), "USD"); } }
Second version of test case using annotations
GlobeTest1.java
package com.kunaal.generics; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.when; import java.util.Map; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; /** * Test case for Globe using Mockito annotation approach * * @author ktrehan */ @RunWith(MockitoJUnitRunner.class) public class GlobeTest1 { @Mock private static Map<String, String> currMap; @Mock private static Map<String, CtryDetails> ctryMap; @InjectMocks private static Globe globeVar; /** * Static initializer */ @BeforeClass public static void setup() { globeVar = new Globe(); globeVar.setCtryMap(ctryMap); globeVar.setCurrMap(currMap); } /** * Test case for ctryType */ @Test public void ctryType() { CtryDetails ctryDetails = new CtryDetails(); ctryDetails.setPopulation(100000l); when(ctryMap.get(anyString())).thenReturn(ctryDetails); assertEquals(globeVar.ctryType("ABCD"), "Not heavily populated"); } /** * Test case for currMap */ @Test public void getCurrency() { when(currMap.get(anyString())).thenReturn("USD"); assertEquals(globeVar.getCurrency("ABCD"), "USD"); } }
Here we are using @Mock and @InjectMocks annotation to plug the generic dependencies.
- How to test parameters passed,usage of ArgumentCaptor
Mockito comes with a concept of capturing the arguments passed to the mocked calls, using ArgumentCaptor, which captures argument values for further assertion.Suppose you want to find the details of mocked method input.ArgumentCaptor helps us in that
BookingDetails.java
Model class for booking details
package com.kunaal.travel; /** * Model class for capturing booking information * * @author ktrehan */ public class BookingDetails { /** * Variable for passenger name */ private String passengerName; /** * Variable for boarding point */ private String boarding; /** * Variable for destination point */ private String destination; /** * Variable for gender */ private String gender; /** * Variable for distance */ private Long distance; /** * @return the distance */ public Long getDistance() { return distance; } /** * @param distance the distance to set */ public void setDistance(Long distance) { this.distance = distance; } /** * @return the passengerName */ public String getPassengerName() { return passengerName; } /** * @param passengerName the passengerName to set */ public void setPassengerName(String passengerName) { this.passengerName = passengerName; } /** * @return the boarding */ public String getBoarding() { return boarding; } /** * @param boarding the boarding to set */ public void setBoarding(String boarding) { this.boarding = boarding; } /** * @return the destination */ public String getDestination() { return destination; } /** * @param destination the destination to set */ public void setDestination(String destination) { this.destination = destination; } /** * @return the gender */ public String getGender() { return gender; } /** * @param gender the gender to set */ public void setGender(String gender) { this.gender = gender; } }
TravelCost.java
Model class for TravelCost
package com.kunaal.travel; /** * Model class for travel cost * * @author ktrehan */ public class TravelCost { /** * Variable for travel type */ private String travelType; /** * Variable for final fare */ private Double finalFare; /** * @return the travelType */ public String getTravelType() { return travelType; } /** * @param travelType the travelType to set */ public void setTravelType(String travelType) { this.travelType = travelType; } /** * @return the finalFare */ public Double getFinalFare() { return finalFare; } /** * @param finalFare the finalFare to set */ public void setFinalFare(Double finalFare) { this.finalFare = finalFare; } /** * @param travelType * @param finalFare */ public TravelCost(String travelType, Double finalFare) { this.travelType = travelType; this.finalFare = finalFare; } }
TravelAgent.java
Class for finding travel cost across different modes
package com.kunaal.travel; import java.util.List; /** * Class for calculating fares across different transportation modes * * @author ktrehan */ public class TravelAgent { /** * Utility method for booking bus ticket * * @param bookingDetails * @return */ public Double bookBusTicket(BookingDetails bookingDetails){ Long distance = bookingDetails.getDistance(); String gender = bookingDetails.getGender(); Double deltaFactor=1d; if(distance >1000 && gender.equals("F")){ deltaFactor=1.05d; }else if(distance <1000 && gender.equals("F")){ deltaFactor=1.1d; }else if(distance >1000 && gender.equals("M")){ deltaFactor=1.07d; }else if(distance <1000 && gender.equals("M")){ deltaFactor=1.14d; } return deltaFactor* 1200; } /** * Utility method for booking train ticket * * @param bookingDetails * @return */ public Double bookTrainTicket(BookingDetails bookingDetails){ Long distance = bookingDetails.getDistance(); String gender = bookingDetails.getGender(); Double deltaFactor=1d; if(distance >1000 && gender.equals("F")){ deltaFactor=1.15d; }else if(distance <1000 && gender.equals("F")){ deltaFactor=1.20d; }else if(distance >1000 && gender.equals("M")){ deltaFactor=1.17d; }else if(distance <1000 && gender.equals("M")){ deltaFactor=1.24d; } return deltaFactor* 2100; } /** * Utility method for booking air ticket * * @param bookingDetails * @return */ public Double bookAirTicket(BookingDetails bookingDetails){ Long distance = bookingDetails.getDistance(); String gender = bookingDetails.getGender(); Double deltaFactor=1d; if(distance >1000 && gender.equals("F")){ deltaFactor=1.2d; }else if(distance <1000 && gender.equals("F")){ deltaFactor=1.22d; }else if(distance >1000 && gender.equals("M")){ deltaFactor=1.27d; }else if(distance <1000 && gender.equals("M")){ deltaFactor=1.34d; } return deltaFactor* 3100; } public void nullifyDistance(List<BookingDetails> inputList){ for(BookingDetails data:inputList){ data.setDistance(0l); } } /** * Utility method for applying group discount * * @param travelCostList * @param percentage * @return */ public boolean applyGpDiscount(List<TravelCost> travelCostList,Double percentage){ for(TravelCost cost:travelCostList){ Double fare = cost.getFinalFare(); Double discount = fare*percentage/100; cost.setFinalFare(fare-discount); } return true; } }
TravelFacade.java
Facade class which interacts with TravelAgent class and get all the reqd information.
package com.kunaal.travel; import java.util.ArrayList; import java.util.List; /** * Facade layer for capturing the information and presenting fares across * different transportation modes * * @author ktrehan */ public class TravelFacade { /** * Variable for travel agent */ private TravelAgent travelAgent; /** * @return the travelAgent */ public TravelAgent getTravelAgent() { return travelAgent; } /** * @param travelAgent * the travelAgent to set */ public void setTravelAgent(TravelAgent travelAgent) { this.travelAgent = travelAgent; } /** * Method for getting travel cost across different travel modes * * @param from * @param to * @param name * @param gender * @return */ public List<TravelCost> getAllTravelCost(String from, String to, String name, String gender) { List<TravelCost> travelCostList = new ArrayList<TravelCost>(); Long distance = 0l; if (from.equals("Bangalore") && to.equals("Delhi")) { distance = 2400l; } else if (from.equals("Bangalore") && to.equals("Chennai")) { distance = 500l; } else { distance = 900l; } BookingDetails bookingDetails = new BookingDetails(); bookingDetails.setBoarding(from); bookingDetails.setDestination(to); bookingDetails.setDistance(distance); bookingDetails.setGender(gender); bookingDetails.setPassengerName(name); Double airFare = travelAgent.bookAirTicket(bookingDetails); Double busFare = travelAgent.bookBusTicket(bookingDetails); Double trainFare = travelAgent.bookTrainTicket(bookingDetails); travelCostList.add(new TravelCost("Air", airFare)); travelCostList.add(new TravelCost("Bus", busFare)); travelCostList.add(new TravelCost("Train", trainFare)); return travelCostList; } }
TravelFacadeTest.java
Test class for TravelFacade.Here, we have a TravelFacade class which has a method getAllTravelCost(....).Input parameters are from,to,passenger name and gender.While fetching the details, we use TravelAgent class with BookingDetails as input.
BookingDetails information comes from input parameters along with other business logic .
Last assertion "assertEquals(new Long(2400), argument.getValue().getDistance());"
shows the usage of ArgumentCaptor.Here we have a logic to get the distance depending upon start and destination place which we verify by argument.getValue().getDistance().
package com.kunaal.travel; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.runners.MockitoJUnitRunner; /** * Mockito has a very nice feature that allows you to verify what parameters * were used when a method was executed. * * @author ktrehan */ @RunWith(MockitoJUnitRunner.class) public class TravelFacadeTest { /** * This test method shows the usage of ARGUMENT CAPTOR STUFF */ @Test public void getAllTravelCost() { TravelFacade facade = new TravelFacade(); TravelAgent mockAgent = mock(TravelAgent.class); facade.setTravelAgent(mockAgent); // Argument Captor ArgumentCaptor<BookingDetails> argument = ArgumentCaptor .forClass(BookingDetails.class); // Input parameters String from = "Bangalore"; String to = "Delhi"; String name = "Test Passenger"; String gender = "F"; // Stubbing when(mockAgent.bookAirTicket(argument.capture())).thenReturn( 1000d); when(mockAgent.bookBusTicket(argument.capture())).thenReturn( 10d); when(mockAgent.bookTrainTicket(argument.capture())).thenReturn( 100d); // Invocation List<TravelCost> allTravelCost = facade.getAllTravelCost(from, to, name, gender); assertEquals(3, allTravelCost.size()); // Verification verify(mockAgent).bookAirTicket(argument.capture()); verify(mockAgent).bookTrainTicket(argument.capture()); verify(mockAgent).bookBusTicket(argument.capture()); // ARGUMENT CAPTOR FEATURE assertEquals(new Long(2400), argument.getValue().getDistance()); } }
- Spy/Partial Mocks
There are some rare where you want to mock some method of implementation.For such scenarios,Mockito has provided spy feature.package com.kunaal.spy; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.List; import org.junit.Test; /** * Test case showing usage of spy feature * * @author ktrehan */ public class SpyTest { /** * Spy feature usage */ @Test public void spyUsage() { List<String> spyList = spy(new ArrayList<String>()); spyList.add("Kunaal"); spyList.add("Kunaal1"); when(spyList.get(anyInt())).thenReturn("Hello World"); assertEquals(spyList.size(), 2); assertEquals(spyList.get(34), "Hello World"); } }
Here in the above method,we are creating a partial mock of ArrayList class.
Here we are invoking actual implementation of add(..) method.However get(..) method is mocked.
- Mockito annotations
Mockito has annotations for most of it features.These are:-
@Mock This is equivalent to mock(.....)
@InjectMocks This injects mock objects in the object
@Spy This is equivalent of spy(.....)
@Captor This is equivalent of ArgumentCaptor.forClass(.....)
However for these annotations to work,either our test class should use
@RunWith(MockitoJUnitRunner.class) or in the setUp() or @Before method
MockitoAnnotations.initMocks(...); should be added
Not all methods returns something.Some methods does not return anything.
Use case has following classes.These are:-
- EntityModel - Model entity class
- IEntityDAO - Interface for persisting,updating and deleting the model information in
data store. - IEntityService - Service interface for persisting,updating and deleting the data
- EntityServiceImpl - Service implementation class having dependency IEntityDAO for
persisting,updating and deleting the data.
EntityModel.java
Model class for storing data information.
package com.kunaal.model;
import java.io.Serializable;
import java.util.Map;
/**
* Model class for entity
*
* @author ktrehan
*/
public class EntityModel implements Serializable {
/**
* Variable for primary key
*/
private Object primaryKey;
/**
* Variable for storing attribute name and value in a map
*/
private Map<String, Object> attributeMap;
/**
* Default Constructor
*/
public EntityModel() {
}
/**
* @return the primaryKey
*/
public Object getPrimaryKey() {
return primaryKey;
}
/**
* @param primaryKey
* the primaryKey to set
*/
public void setPrimaryKey(Object primaryKey) {
this.primaryKey = primaryKey;
}
/**
* @return the attributeMap
*/
public Map<String, Object> getAttributeMap() {
return attributeMap;
}
/**
* @param attributeMap
* the attributeMap to set
*/
public void setAttributeMap(Map<String, Object> attributeMap) {
this.attributeMap = attributeMap;
}
}
IEntityDAO.java
DAO interface for persisting,deleting and updating the data in data store.
package com.kunaal.dao;
import com.kunaal.model.EntityModel;
/**
* DAO interface for persisting,updating and deleting the model
*
* @author ktrehan
*/
public interface IEntityDAO {
/**
* Method for insertion the data
*
* @param model
* @return
*/
public Object insert(EntityModel model);
/**
* Method for updating the data
*
* @param model
*/
public void update(EntityModel model);
/**
* Method for deleting the data
*
* @param model
* @return
*/
public boolean delete(EntityModel model);
}
IEntityService.javaService interface for insertion,deletion and updation.
package com.kunaal.service;
import com.kunaal.model.EntityModel;
/**
* Service interface for insert,update and deletion
*
* @author ktrehan
*/
public interface IEntityService {
/**
* Method for insertion the data
*
* @param model
* @return
*/
public Object insert(EntityModel model);
/**
* Method for updating the data
*
* @param model
*/
public void update(EntityModel model);
/**
* Method for deleting the data
*
* @param model
* @return
*/
public boolean delete(EntityModel model);
}
EntityServiceImpl.java
Service implementation having dependency on DAO class which handles insertion,updation and deletion logic.
package com.kunaal.service.impl;
import com.kunaal.dao.IEntityDAO;
import com.kunaal.model.EntityModel;
import com.kunaal.service.IEntityService;
/**
* Service implementation delegating the calls to DAO
*
* @author ktrehan
*/
public class EntityServiceImpl implements IEntityService {
// Variable for entityDAO
private IEntityDAO entityDAO;
/**
* Insert method
*/
public Object insert(EntityModel model) {
return getEntityDAO().insert(model);
}
/**
* Update method
*/
public void update(EntityModel model) {
getEntityDAO().update(model);
}
/**
* Delete method
*/
public boolean delete(EntityModel model) {
return getEntityDAO().delete(model);
}
/**
* @return the entityDAO
*/
public IEntityDAO getEntityDAO() {
return entityDAO;
}
/**
* @param entityDAO
* the entityDAO to set
*/
public void setEntityDAO(IEntityDAO entityDAO) {
this.entityDAO = entityDAO;
}
}
EntityServiceTest.java
Test class for EntityServiceImpl.java
package com.kunaal.service;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.Map;
import org.junit.BeforeClass;
import org.junit.Test;
import com.kunaal.dao.IEntityDAO;
import com.kunaal.model.EntityModel;
import com.kunaal.service.impl.EntityServiceImpl;
/**
* @author ktrehan
*
*/
public class EntityServiceTest {
// Variable for entity service
private static EntityServiceImpl entityService;
// Variable for entity DAO
private static IEntityDAO entityDAO;
/**
* Static initializer where setup happens
*/
@BeforeClass
public static void setup() {
entityDAO = mock(IEntityDAO.class);
entityService = new EntityServiceImpl();
entityService.setEntityDAO(entityDAO);
}
/**
* Test method for void update method
*/
@Test
public void update() {
EntityModel model = new EntityModel();
Map<String, Object> attributeMap = model.getAttributeMap();
model.setPrimaryKey(new Long(1));
attributeMap.put("Name", "TestCase");
attributeMap.put("Address", "India");
// MOCKING VOID METHOD UN ENTITYDAO
doNothing().when(entityDAO).update(model);
entityService.update(model);
assertEquals(new Long(1), model.getVersion());
verify(entityDAO, times(1)).update(model);
}
}
Here we are faking update(..) method present under IEntityDAO.Update method is a void method.
So syntax for faking is little different .Instead of when(Mock.method(...).thenReturn(...) ,we are using doNothing().when(Mock.method(...))
Restrictions of Mockito
Mockito can't do following things.These are:
- can't mock static classes
- can't mock final classes
- can't mock static methods
- can't mock equals and hashCode
Old method of overcoming Mockito Limitations
Since Mockito and other such frameworks can't mock static,final classes and methods.So developers use to write reflection utility to over come this problem
This reflection utility serves following purposes.These are:-
- Setting values for static final variables
- Setting field value of static variable
- Setting field value of private variable
- Getting field value of private variable
- Getting response from private method invocation.
ReflectionUtility.java
package com.kunaal;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
/**
* @author ktrehan
*
*/
public class ReflectionUtil {
/**
* Utility method to find value of private field
*
* @param fieldName
* @param instance
* @return
* @throws SecurityException
* @throws NoSuchFieldException
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
public static Object getFieldValue(Class classVal, String fieldName,
Object instance) throws Exception {
Field field = classVal.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(instance);
}
/**
* Utility method for finding value of private method invocation.
*
* @param methodName
* @param instance
* @return
* @throws SecurityException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
public static Object getMethodValue(Class classVal, String methodName,
Object instance, Class[] classArgArr, Object[] valArgArr)
throws Exception {
Method method = null;
Object returnVal = null;
if (classArgArr == null || classArgArr.length == 0) {
method = classVal.getDeclaredMethod(methodName, null);
} else {
method = classVal.getDeclaredMethod(methodName,
classArgArr);
}
method.setAccessible(true);
if (valArgArr == null || valArgArr.length == 0) {
returnVal = method.invoke(instance, null);
} else {
returnVal = method.invoke(instance, valArgArr);
}
return returnVal;
}
/**
* Utility method to set value of private field
*
* @param fieldName
* @param instance
* @return
* @throws SecurityException
* @throws NoSuchFieldException
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
public static void setFieldValue(Class classVal, String fieldName,
Object instance, Object mockVal) throws Exception {
Field field = classVal.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(instance, mockVal);
}
/**
* Utility method for setting static field value
*
* @param classVal
* @param fieldName
* @param mockVal
* @throws Exception
*/
public static void setStaticFieldValue(Class classVal,
String fieldName, Object mockVal) throws Exception {
Field field = classVal.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(null, mockVal);
}
/**
* Utility method for setting static final variable
*
* @param classVal
* @param fieldName
* @param mockVal
* @throws Exception
*/
public static void setFinalStatic(Class classVal, String fieldName,
Object mockVal) throws Exception {
Field field = classVal.getDeclaredField(fieldName);
field.setAccessible(true);
// remove final modifier from field
Field modifiersField = Field.class
.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers()
& ~Modifier.FINAL);
field.set(null, mockVal);
}
}
Use Case:-
Here SchoolAdmission class follows template pattern.
Method admissionStatus(.....) calls following private methods.
- submitForm(AdmissionDataModel)
- formVerified(AdmissionDataModel)
- ageConditionMet(AdmissionDataModel)
- parentQualMet(AdmissionDataModel)
- feesConditionsAgreed(AdmissionDataModel)
AdmissionDataModel.java
Model class for kid admission attributes.
package com.kunaal.admission;
import java.util.Calendar;
import java.util.Map;
/**
* Model class for kid admission attributes
*
* @author ktrehan
*/
public class AdmissionDataModel {
/**
* Variable for kid name
*/
private String kidName;
/**
* Variable for parent qualifications
*/
private Map<String,String> parentQualMap;
/**
* Variable for parent's approval for annual fee hike if any
*/
private Boolean annualFeeHike;
/**
* Variable for kid's date of birth
*/
private Calendar dob;
/**
* Variable for checking whether form is fully filled or not
*/
private Boolean formFullyFilled;
/**
* Variable for form submission date
*/
private Calendar formSubDate;
/**
* Parameterized constructor
*
* @param kidName
* @param parentQualMap
* @param annualFeeHike
* @param dob
* @param formFullyFilled
* @param formSubDate
*/
public AdmissionDataModel(String kidName,
Map<String, String> parentQualMap, Boolean annualFeeHike,
Calendar dob, Boolean formFullyFilled, Calendar formSubDate) {
this.kidName = kidName;
this.parentQualMap = parentQualMap;
this.annualFeeHike = annualFeeHike;
this.dob = dob;
this.formFullyFilled = formFullyFilled;
this.formSubDate = formSubDate;
}
/**
* @return the kidName
*/
public String getKidName() {
return kidName;
}
/**
* @return the parentQualMap
*/
public Map<String, String> getParentQualMap() {
return parentQualMap;
}
/**
* @return the annualFeeHike
*/
public Boolean getAnnualFeeHike() {
return annualFeeHike;
}
/**
* @return the dob
*/
public Calendar getDob() {
return dob;
}
/**
* @return the formFullyFilled
*/
public Boolean getFormFullyFilled() {
return formFullyFilled;
}
/**
* @return the formSubDate
*/
public Calendar getFormSubDate() {
return formSubDate;
}
}
SchoolAdmission.java
package com.kunaal.admission;
import java.util.Calendar;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.Map;
/**
* Template class for school admission
*
* @author ktrehan
*/
public class SchoolAdmission {
/**
* Method for finding admission status
*
* @param dataModel
* @return
*/
public String admissionStatus(AdmissionDataModel dataModel) {
StringBuffer buffer = new StringBuffer();
boolean submitForm = submitForm(dataModel);
if (!submitForm) {
buffer.append("Form not submitted on time");
}
boolean formVerified = formVerified(dataModel);
if (!formVerified) {
buffer.append("Form is not filled properly");
}
boolean ageConditionMet = ageConditionMet(dataModel);
if (!ageConditionMet) {
buffer.append("Your kid does not fulfil age conditions");
}
boolean parentQualMet = parentQualMet(dataModel);
if (!parentQualMet) {
buffer.append("Basic parent's qualification not met");
}
boolean feesConditionsAgreed = feesConditionsAgreed(dataModel);
if (!feesConditionsAgreed) {
buffer.append("Special condition of annual fee hike(if any) is not checked in form");
}
if (buffer.length() == 0)
buffer.append("Your kid's admission status - SUCCESS");
return buffer.toString();
}
/**
* Private method for submitting admission form
*
* @param dataModel
* @return
*/
private boolean submitForm(AdmissionDataModel dataModel) {
Calendar formSubDate = dataModel.getFormSubDate();
Calendar finalDate = GregorianCalendar.getInstance();
finalDate.set(Calendar.YEAR, 2013);
finalDate.set(Calendar.MONTH, Calendar.JANUARY);
finalDate.set(Calendar.DAY_OF_MONTH, 1);
if (formSubDate.before(finalDate)) {
return true;
} else {
return false;
}
}
/**
* Private method for verifying the form
*
* @param dataModel
* @return
*/
private boolean formVerified(AdmissionDataModel dataModel) {
return true;
}
/**
* Private method for age condition check
*
* @param dataModel
* @return
*/
private boolean ageConditionMet(AdmissionDataModel dataModel) {
Calendar kidDob = dataModel.getDob();
Calendar finalDate = GregorianCalendar.getInstance();
int birthYear = kidDob.get(Calendar.YEAR);
int currYear = finalDate.get(Calendar.YEAR);
if ((currYear - birthYear) >= 4) {
return true;
} else {
return false;
}
}
/**
* Private method for parent qualification check
*
* @param dataModel
* @return
*/
private boolean parentQualMet(AdmissionDataModel dataModel) {
Map<String, String> parentQualMap = dataModel
.getParentQualMap();
boolean result = false;
Collection<String> values = parentQualMap.values();
for (String data : values) {
if (data.toString().equals("MBA")) {
result = true;
break;
}
}
return result;
}
/**
* Private method for fees conditions check
*
* @param dataModel
* @return
*/
private boolean feesConditionsAgreed(AdmissionDataModel dataModel) {
return true;
}
}
Unit Test case for testing private method parentQualMet() using reflection utility
package com.kunaal.admission;
import static org.junit.Assert.assertTrue;
import java.util.HashMap;
import java.util.Map;
import org.junit.BeforeClass;
import org.junit.Test;
import com.kunaal.ReflectionUtil;
/**
* This test illustrates testing of private methods using reflection api
*
* @author ktrehan
*/
public class SchoolAdmissionTest {
/**
* Variable for SchoolAdmission
*/
private static SchoolAdmission schoolAdmission;
/**
* Before class set up
*/
@BeforeClass
public static void setUp() {
schoolAdmission = new SchoolAdmission();
}
/**
* Testing private method parentQualMet()
*
* @throws Exception
*/
@Test
public void parentQualMet() throws Exception {
AdmissionDataModel dataModel = new AdmissionDataModel(null,
null, null, null, null, null);
// Setting dummy map
Map<String, String> dummyMap = new HashMap<String, String>();
dummyMap.put("Father", "MBA");
// Set the particular private field
ReflectionUtil.setFieldValue(AdmissionDataModel.class,
"parentQualMap", dataModel, dummyMap);
// Invoking private method
Class[] classArgArr = new Class[] { AdmissionDataModel.class };
Object[] valArgArr = new Object[] { dataModel };
Object methodValue = ReflectionUtil.getMethodValue(
SchoolAdmission.class, "parentQualMet",
schoolAdmission, classArgArr, valArgArr);
assertTrue(methodValue instanceof Boolean);
assertTrue((Boolean) methodValue);
}
}
Here we are using reflection to set value for private variable "parentQualMap" and invoking private method "parentQualMet".However now we have PowerMock which does it nicely
PowerMock and its features
PowerMock is a framework which uses a custom classloader and bytecode manipulation to enable mocking of static methods, constructors, final classes and methods, private methods, removal of static initializers and more. By using a custom classloader no changes need to be done to the IDE or continuous integration servers which simplifies adoption.
PowerMock Features
- Mocking static methods
- Mocking private methods
- Mocking new object creation inside method
- Mocking final
Lets revisit the use case described above and use PowerMock for invoking private method and setting private variables
Testcase using Whitebox utility provided by PowerMock for testing private methods.
package com.kunaal.admission;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.HashMap;
import java.util.Map;
import org.junit.BeforeClass;
import org.junit.Test;
import org.powermock.reflect.Whitebox;
/**
* This test illustrates the use of power mock whitebox method for testing
* private methods and setting values in private fields
*
* @author ktrehan
*/
public class SchoolAdmission1 {
/**
* Variable for SchoolAdmission
*/
private static SchoolAdmission schoolAdmission;
/**
* Before class set up
*/
@BeforeClass
public static void setUp() {
schoolAdmission = new SchoolAdmission();
}
/**
* Testing private method parentQualMet()
*
* @throws Exception
*/
@Test
public void parentQualMet() throws Exception {
AdmissionDataModel dataModel = new AdmissionDataModel(null,
null, null, null, null, null);
Map<String, String> dummyMap = new HashMap<String, String>();
dummyMap.put("Father", "MBA");
// Whitebox usage for setting private field
Whitebox.setInternalState(dataModel, "parentQualMap", dummyMap);
// Whitebox usage for invoking private methods
Boolean invokeMethod = Whitebox.<Boolean> invokeMethod(
schoolAdmission, "parentQualMet", dataModel);
assertEquals(dataModel.getParentQualMap(), dummyMap);
assertTrue(invokeMethod);
}
}
Use case of mocking a static method
In this use case we will unit test a static method.Here we are taking example of Singleton class where getInstance() and getData() method are static methods.
Singleton.java
Singleton class having getInstance() and getData() as static methods.
package com.kunaal.singleton;
/**
* Singleton class example
*
* @author ktrehan
*/
public class Singleton {
/**
* Variable for singleton
*/
private static Singleton INSTANCE = new Singleton();
/**
* Private constructor
*/
private Singleton() {
}
/**
* Static method for getting instance object
*
* @return
*/
public static Singleton getInstance() {
return INSTANCE;
}
/**
* Dummy static getData() method
*
* @return
*/
public static String getData() {
return null;
}
}
SingletonTest.java
Test case for singleton class using PowerMock.
package com.kunaal.singleton;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
/**
* Test class illustrating usage of mocking static method using power mock
*
* @author ktrehan
*/
@RunWith(PowerMockRunner.class)
public class SingletonTest {
/**
* Testing getInstance() of Singleton
*/
@Test
@PrepareForTest(Singleton.class)
public void getInstance() {
PowerMockito.mockStatic(Singleton.class); // Mock all the static methods
// of Singleton
Singleton mockObj = mock(Singleton.class); // Mock the singleton class
when(Singleton.getInstance()).thenReturn(mockObj); // Push the
// expectations
assertEquals(Singleton.getInstance(), mockObj);// Assertions
PowerMockito.verifyStatic(times(1));// Verification for each method
Singleton.getInstance();
PowerMockito.verifyStatic(Mockito.never());// Verification for each
// method
Singleton.getData();
}
}
Use case of mocking object creation
InnerObject.java
Here in listSize() method, we are creating a new object.Depending upon the object state we are returning true/false.
package com.kunaal.mocknew;
import java.util.ArrayList;
import java.util.List;
/**
* Class which creates a new object inside a method
*
* @author ktrehan
*/
public class InnerObject {
/**
* Method for finding list size where object is created inside
*
* @return
*/
public boolean listSize(){
List<String> dataList=new ArrayList<String>();
if(dataList.size() >0)
return false;
else
return true;
}
}
InnerObjectTest.java
This test class shows the usage of PowerMock ability to mock new object creation.
package com.kunaal.mocknew;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
/**
* Test case showing usage of Power Mock new object feature Here in
* InnerObject's listSize() method we create a new array list object This is
* mocked here
*
* @author ktrehan
*/
@RunWith(PowerMockRunner.class)
public class InnerObjectTest {
@Test
@PrepareForTest(InnerObject.class)
public void listSize() throws Exception {
ArrayList mockList = mock(ArrayList.class); // mock arraylist object
// which is returned
// by constructor invocation
PowerMockito.whenNew(ArrayList.class).withNoArguments()
.thenReturn(mockList);// Using PowerMock to mock
// arraylist constructor
when(mockList.size()).thenReturn(0);// setting expectation of mock
InnerObject obj = new InnerObject();
assertTrue(obj.listSize());
Mockito.verify(mockList, Mockito.times(1)).size();
// Using PowerMock to verify constructor invocation
PowerMockito.verifyNew(ArrayList.class, times(1)).withNoArguments();
}
}
Use case of mocking private methods
PrivateCall.javaHere we have a public method which internally uses private method for some computation.
package com.kunaal.mockprivate;
import java.util.List;
/**
* Class where a public method outsourced main logic to private method
*
* @author ktrehan
*/
public class PrivateCall {
/**
* Public method which invokes private method for business logic
*
* @param inputList
* @param valToChk
* @return
*/
public boolean find(List<String> inputList,String valToChk){
if(inputList !=null)
return isExists(inputList, valToChk);
else
return false;
}
/**
* Private method having main logic
*
* @param inputList
* @param valToChk
* @return
*/
private boolean isExists(List<String> inputList,String valToChk){
return inputList.contains(valToChk);
}
}
PrivateCallTest.java
Test class which uses PowerMock to mock the private method .
package com.kunaal.mockprivate;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
/**
* Test case showing usage of Power Mock's private method testing
*
* @author ktrehan
*/
@RunWith(PowerMockRunner.class)
public class PrivateCallTest {
@Test
@PrepareForTest(PrivateCall.class)
public void find() throws Exception {
PrivateCall underTest = PowerMockito.spy(new PrivateCall());// Using
// power
// mock spy
// feature
// Input data
List<String> inputList = new ArrayList<String>();
String valToChk = new String();
// Setting expectation of private method call
PowerMockito.when(underTest, "isExists", inputList, valToChk)
.thenReturn(Boolean.TRUE);
assertTrue(underTest.find(inputList, valToChk));
// Optionally verify that the private method was actually called
PowerMockito.verifyPrivate(underTest).invoke("isExists", inputList,
valToChk);
}
}
Awesome post...it covers the whole of unit testing in a very simple and extensive manner....thanks
ReplyDeleteThe article was nice and explanation is clear and more realistic
ReplyDeleteAwsome article man...... great job :)
ReplyDeleteCould you please write some example on nodejs.
ReplyDeletenice article...................................
ReplyDeleteNice explanation of the blog
ReplyDeleteSanjary Kids is one of the best play school and preschool in Hyderabad,India. Give your child the best preschool experience by choosing the best playschool of Hyderabad in Abids. we provide programs like Play group,Nursery,Junior KG,Senior KG,and provides Teacher Training Program.
early childhood teacher training course in hyderabad
Great blog posts information I liked it
ReplyDeleteSanjary Academy is the best Piping Design institute in Hyderabad, Telangana. It is the best Piping design Course in India and we have offer professional Engineering Courses like Piping design Course, QA/QC Course, document controller course, Pressure Vessel Design Course, Welding Inspector Course, Quality Management Course and Safety Officer Course.
best Piping Design Course
piping design course with placement
Piping Design Course
Piping Design Course in Hyderabad
Piping Design Course in India
best Piping Design institute
best institute of Piping Design Course in India
Excellent blog posts of the blog provided
ReplyDeleteSanjary Academy provide pressure vessel design,quality management system course, piping design course, qa/qc course and document controller course.
Welding Inspector Course
Safety officer course
Quality Management Course
Quality Management Course in India
Awesome article, it was exceptionally helpful! I simply began in this and I'm becoming more acquainted with it better! Cheers, keep doing awesome
ReplyDeleteBCOM 1st Year TimeTable 2020
Excellent post. I was always checking this blog, and I’m impressed! Extremely useful info specially the last part, I care for such information a lot. I was exploring this particular info for a long time. Thanks to this blog my exploration has ended.
ReplyDeleteIf you want Digital Marketing Serives :-
Digital marketing Service in Delhi
SMM Services
PPC Services in Delhi
Website Design & Development Packages
SEO Services PackagesLocal SEO services
E-mail marketing services
YouTube plans
Excellent post. I was always checking this blog hair transplantation success rate
ReplyDeleteThanks for choosing this specific Topic. As i am also one of big lover of this. Your explanation in this context is amazing. Keep posted these overwarming facts in front of the world.
ReplyDeletePrinter customer support number
Respect and I have a super proposal: How Much House Renovation Cost Philippines residential renovation contractors near me
ReplyDelete