Unit testing plays a very important role in any successful software development. If you strictly follow the Test Driven Development methodology, you should code unit test first before writing any component. The build or release will be successful only if all the unit tests pass. This is a good practice not just for the initial phase of development but also for the future enhancement. Test code will help prevent any defects introduced by the changes for the new features. The unit test code essentially becomes a valuable asset to relieve the burden of regression testing.
Another important aspect of unit testing is it is usually very quick and cheap to run. Unlike integration test, unit test is supposed to be lightweight, not dependent on downstream systems (such as database), and repeatable as it will not have any side effect on the system.
Now you may ask this question. Is it easy to write unit test for IdentityIQ or IdentityNow? The answer is probably not. For components like Workflow, Application, Bundle or Task, it may not be possible to write pure unit test code (yes you can write integration tests which are executed in a real IdentityIQ orIdentityNow environment by using some testing tools/frameworks developed by SailPoint community). But for Rules, there are ways to write unit test code.
One of big challenges that stops us from writing any meaningful unit test is that Rule is written in Beanshell programing language. Beanshell, as a scripting language, gives you a lot of flexibilities to update logic in runtime without requiring application rebuilding or redeployment. But its downside is it doesn't provide much support for unit testing. Another challenge is Rule needs an environment (IdentityIQ or IdentityNow) to run. So without a simulator, the only way to test a Rule is to run the Rule in a full-fledged IdentityIQ installation or IdentityNow tenant. Based on the previous discussion, this way of testing doesn't qualify as true unit testing. So now how to address these 2 main challenges?
To address the first challenge, we need to look from the Rule manufacturing point of view. How do we create the Rule? The Beanshell code of Rule is essentially a snippet of Java code. In fact, many of us tend to write the body of Rule in a separate Java project with the help of IDE (Integrated Development Environment) software like Eclipse or IntelliJ, and then copy that part of code to an XML file with proper wrapper to save it as a legitimate Rule file. Sail4j is a tool to do the similar thing but in a automated fashion, and seamlessly integrated with the building process commonly used in an IdentityIQ or IdentityNow project. For example, if we have an IdentityIQ project using SSB (Standard Service Build), then a Custom Apache Ant Task from Sail4j can be configured in the build.xml to convert a Java class to Rule XML. Similarly, if you happen to use Maven in an IdentityNow or IdentityNow project, the Maven Plugin from Sail4j can be used. Now, if you take a step back and look at this from a software development perspective, the Java class becomes your source code and Beanshell Rule XML is the output of the build. This way of development switch provides 2 obvious big benefits:
Refer to the following page for more details about how to use and setup Sail4j in an IdentityIQ or IdentityNow project.
https://github.com/renliangfeng/sail4j-iiq-idn
Before touching the second challenge mentioned earlier, I would like to talk a bit about unit testing. One key principle of unit testing is it should ONLY test the component that needs to be tested. Taking a 2 layers application as an example, you have a service layer component and a data access layer component where service layer calls the functions/methods from data access layer. When you write unit test for the service, you should assume everything in data access layer functions exactly as it is supposed to behave. If you try to code to connect data access component to the Database to read to write data in you test case, then you are effectively testing both service and data access components which theoretically categorize it an integration test rather than unit test. So how can you write a unit test without having to write code to get the dependent components and downstream systems running? The key technique is to mock the downstream layer. In this example, you can simulate the methods/functions of data access component in you unit test code and trick your service to execute the scenarios you would like to test.
We can apply the same techniques to an IdentityIQ or IdentityNow project. Here we want to unit test the Rule and the Rule's downstream is IdentityIQ or IdentityNow runtime environment. If you take a close look at most of Rules you have implemented, the commonly used SailPoint objects are SailPointContext, IdentityService etc. So if we can find a way to mock these SailPoint objects, then we should be able to write unit tests. When it comes to Java API mocking, one of my favorite frameworks is Mockito. It provides a rich set of features and user-friendly APIs for mocking to suit different testing scenarios and works very well with JUnit testing framework. In the following sections, I will break down into multiple use cases where you can use different features of Mockito to mock the SailPoint object to test your Rules.
One of the main features of Mockito is to mock a Java interface. Suppose you want to test a Java class which calls a method from a Java interface, and the Java class has an object reference with type of that Java interface. In your JUnit test case you can mark that object reference with Mockito Mock annotation, then Mockito will automatically instantiate a mocking stub object of that interface. Then you can use Mockito to setup the expected behaviors of the stub object to suit your different testing scenarios. One of the most commonly used SailPoint interface in Rule is SailPointContext. Below is a simple example of mocking the method getObjectByName of SailPointContext in a JUnit test case using org.mockito.Mockito.when method. The Rule we will test here is called MyRule which calculates the value of Display Name based on firstName, lastName and middleName.
.
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
...
@Mock
SailPointContext context;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Test
public void testDisplayName() throws Exception {
Identity identity = new Identity();
identity.setFirstname("Jane");
identity.setLastname("Doe");
when (context.getObjectByName(Identity.class, "JaneDoe")).thenReturn(identity);
MyRule myRule = new MyRule();
myRule.context = this.context;
assertEquals("Doe, Jane",myRule.getDisplayName(context,identity));
}
In another more complicated case, you may have a Rule (Identity Attribute) to generate a unique username. To make the username unique, the Rule calls the search method of SailPointContext to validate if the potential username is already used. To simulate the searching result for the testing scenarios that username is already used, the following snippet of code will achieve the goal. With small modification, you can easily mock other scenarios that you would like to cover.
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
...
QueryOptions queryOptions = new QueryOptions();
queryOptions.addFilter(Filter.eq("application.name", applicationName));
queryOptions.addFilter(Filter.ignoreCase(Filter.eq("searchableUsername", userName)));
List<Object[]> lst = new ArrayList<Object[]>();
if (!expectEmpty) {
String nativeIdentity = "test";
Attributes attributes = new Attributes();
attributes.put("id", "dummyid001");
Object[] array = new Object[2];
array[0] = nativeIdentity;
array[1] = attributes;
lst.add(array);
}
when(context.search(Link.class, queryOptions, "id")).thenReturn(lst.iterator());
Mock function of Mockito is relatively easy to understand and use. However Mock function only works for Java Interface (such as sailpoint.api.SailPointContext in the previous examples). If the Rule uses Java class like sailpoint.api.IdentityService, then you can't use Mockito's Mock function. In general, this type of Rule (or the method of Rule library) is not testable (at least from unit testing perspective). That being said, with a bit of tweak, the Rule can become testable largely with the help of another Mockito feature called Spy. The technique I normally use is to extract that section of code (untestable due to use of unmockable Java class of SailPoint APIs) into a separate method. Then in JUnit test, you spy the Rule object itself with Mockito Spy annotation. Spy essentially allows you to mock the methods (which are untestable) of the Java class that you are testing so that you can still test the remaining parts. In this example, you mock the behavior of new method with code of calling IdentityService (this part of code is not testable and we just have to accept that), but you can still test the other parts of Rule. Now let's use an example to demonstrate how to use Spy in JUnit test. Suppose you have a Rule that is used to get email address in a writeback process (to a target system after provision). The email fetching has following logic:
The Rule is developed in a Java class as shown below:
@SailPointRule(name = "Rule-Get-Generated-Email", fileName = "Rule-Get-Generated-Email.xml")
public class RuleGetGeneratedEmail {
private static Log ruleLog = LogFactory.getLog("rule.GetGeneratedEmail");
@IgnoredBySailPointRule
SailPointContext context;
List<Link> getLinks(Identity identity, Application app) throws GeneralException {
IdentityService idService = new IdentityService(context);
List<Link> adLinks = idService.getLinks(identity, app);
return adLinks;
}
public String fetchEmail(Identity identity) throws GeneralException {
String emailFromIdentity = identity.getEmail();
if (StringUtils.isNotBlank(emailFromIdentity)) {
ruleLog.debug("Get email from Identity email attribute: " + emailFromIdentity);
return emailFromIdentity;
} else {
String primaryAdAppName = (String)identity.getAttribute("primaryAdApplication");
Application primaryAdApplication = null;
if (StringUtils.isNotBlank(primaryAdAppName)) {
primaryAdApplication = context.getObjectByName(Application.class, primaryAdAppName);
}
if (primaryAdApplication != null) {
List<Link> adLinks = getLinks(identity, primaryAdApplication);
if (adLinks != null && adLinks.size() == 1) {
Link primaryAdAccount = adLinks.get(0);
String emailFromAD = (String)primaryAdAccount.getAttribute("mail");
if (StringUtils.isNotBlank(emailFromAD)) {
ruleLog.debug("Get email from primary AD mail attribute: " + emailFromAD);
return emailFromAD;
}
}
}
}
return "";
}
}
As you can see, the first method getLinks is untestable as it uses a SailPoint class IdentityService which is un-mockable (note: this is based on my knowledge of Mockito, maybe it's possible by using other mocking frameworks). So now in my JUnit test case, I will use Spy annotation to spy the whole Rule which allows me to simulate the method getLinks using method org.mockito.Mockito.doReturn.
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
...
public class RuleGetGeneratedEmailTest {
@Mock
SailPointContext context;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Spy
RuleGetGeneratedEmail rule = new RuleGetGeneratedEmail();
Identity identity;
Application adApp = new Application();
List<Link> adLinks = new ArrayList<Link>();
@Before
public void setUp() throws Exception {
rule.context = context;
identity = new Identity();
identity.setFirstname("Test");
identity.setLastname("Smith");
identity.setName("Test.Smith");
adApp.setName("Active Directory");
}
@Test
public void testEmailFromAD() throws Exception {
when (context.getObjectByName(Application.class, "Active Directory")).thenReturn(adApp);
identity.setAttribute("primaryAdApplication", "Active Directory");
Link adLink = new Link();
adLink.setAttribute("mail", "abc.efg@gmail.com");
adLinks.add(adLink);
doReturn(adLinks).when(rule).getLinks(identity, adApp);
String fetchedEmail = rule.fetchEmail(identity);
assertEquals("abc.efg@gmail.com", fetchedEmail);
}
The method Mockito.verify is another feature commonly used to validate the logic in Unit test cases. It allows you to validate expected arguments are passed when the method of a mocked or spied object is called. You can also specify exactly how many times the method is expected to be called. This can be useful to test the provisioning related Rules such as Before Provision Rule or After Provision Rule. For example, you have a After Provision Rule which writes back some attributes to a HR application once Active Directory account is provisioned successfully.
@SailPointRule(name = "Rule-Writeback", fileName = "Rule-Writeback.xml",
type = RuleType.AFTER_PROVISIONING, referencedRules = {"Rule-Library-Writeback"})
public class RuleWriteback {
@IgnoredBySailPointRule
SailPointContext context;
@IgnoredBySailPointRule
public String writeback(String empId, Map attributes) {
return null;
}
@SailPointRuleMainBody
public void execute(ProvisioningPlan plan, Application application, ProvisioningResult result)
throws Exception {
//complex code is ommitted here; our main goal of unit testing is to test the code included here.
writeback(employeeID, attributes);
}
}
In this example, the writeback method is inside a referenced Rule library and will not be tested in this Unit test (and it is not testable anyway). But at least we can validate if the arguments passed to this method are correct by calling Mockito.verify with the expected values as shown in the JUnit test code snippet below:
@Spy
RuleWriteback rule = new RuleWriteback();
@Test
public void testSuccessJoiner() throws Exception {
sailpoint.object.ProvisioningPlan plan = init(sailpoint.object.ProvisioningPlan.AccountRequest.Operation.Create);
addAttribute("mail", "test.smith@gmail.com");
addAttribute("employeeID", "4000871");
addAttribute("sAMAccountName", "P4000871");
rule.execute(plan, adApplication, null);
Map map = new HashMap();
map.put("PrimaryEmail", "test.smith@gmail.com");
map.put("LogonUserName", "P4000871");
Mockito.verify(rule).writeback("4000871", map);
}
To build some sophisticated test cases, you can even use Mockito ArgumentCaptor to capture arguments passed to a method that is mocked or spied. This allows you to examine the arguments to validate real values against expected values. Refer to the Mockito document for more details about how to use this function.
The sailpoint.object.Custom object is quite often used in a Rule. In JUnit test case for the Rule, you can write code to populate the Custom object to reflect what's defined in the Custom XML, this will work just fine. However there is a better way to do this. You can use the method populateCustomFromFile from the following test helper class provided by Sail4j to populate the values from the real Custom XML file:
com.sailpoint.sail4j.test.Sail4jUnitTestHelper
Below example shows how to use this helper class to populate a SailPoint Custom object in JUnit before test method. The benefit of this not only reduces the lines code in your test case, but also allows you to test the Custom object itself. On top of that, when there are changes in the Custom object in the future, you don't need to update JUnit tests to reflect the changes.
import sailpoint.api.SailPointContext;
import sailpoint.object.Custom;
import com.sailpoint.sail4j.test.Sail4jUnitTestHelper;
..
@Mock
SailPointContext context;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
MyRule rule = new MyRule();
@Before
public void setUp() throws Exception {
Custom buCustom = Sail4jUnitTestHelper.populateCustomFromFile("config/Custom/Custom-BusinessUnit.xml");
rule.context = context;
when (context.getObject(Custom.class, "Custom-BusinessUnit")).thenReturn(buCustom);
}
The test helper class Sail4jUnitTestHelper is included in the Sail4j test helper module. To use it, you need to include the following jar file in your project. Refer to the link provided in the previous section for details about how to setup and use Sail4j in your project.
sail4j-test-helper-1.1.jar
The goal of unit testing in an IdentityIQ or IdentityNow project is never going to have the 100 or 90 percentage testing coverage due to the fact that we can't mock all SailPoint APIs used in the Rules plus other constraints. But as you have discovered in the previous sections, with a bit of creativity, you can write code to test the majority of logic. Some of these tweaks may seem to be inconvenient, but in my view it is usually align with the best coding practice in general. It makes code more testable, manageable and readable. As a true believer of TDD (Test Driven Development) methodology, I always think having unit testing is alway better than no unit testing.
Another thing I want to point out is Mockito and JUnit are just the tools that I personally like and am more familiar with. There are many other excellent frameworks in the market you can consider which may better suit your needs or styles. Once you start writing Rule in plain Java code, the door is open and the world is your oyster. There are a lot of open-source solutions for Java unit testing you can explore. For example, in my old days as a developer before discovering Mockito, I have used EasyMock which was pretty good as well. As an alternative unit testing framework to JUnit, TestNG is probably worth checking out as well.
Hello @ajeet_bunny, The rule you included signatures and related attributes. How to generate signatures using sail4j-java?