Learn About Sanity Testing With TestNG

Optimizing execution time for test builds is the all-time hot topic for test automation engineers, and organizing tests and their dependencies plays a crucial part. So, this article showcases the use-cases of Sanity tests with some practical highlights and usage.
 
Quite recently, I have faced a requirement to minimize the overall test execution time for the unit and integration testing jobs that were running overnight. Generally, there are two ways in which we can figure out how to minimize the overall test execution time.
  1. Writing your test cases in a way that they execute independently and fast. This becomes possible by using less redundant validations, annotations such as skip exceptions, and soft or hard assertions. However, going deeper into the explanation of this topic is not going to be covered in this article.
  2. Designing overall project structure in a way that it gives a look and feel of optimized and organized independent test suites (groups or categories – based on the individual product feature). This becomes possible when we include concepts such as priority test execution, groups, depends on, etc. in our project structure while designing the testing framework. And this is what we are going to talk about in this short article – the sanity testing using the concept of grouping and depending on the feature of TestNG.
That said, before proceeding, note that TestNG is a testing framework to perform end-to-end testing, including Unit, Component, Integration, Regression, UI, etc. That means, it offers a wide range of features to cater to the requirements of all these test types. However, we will only be talking about Sanity testing.
 

Sanity Testing

 
Sanity represents the rationality measure and mental health of a person (object). This drives the concept of sanity testing. Sanity testing refers to some tests which should run ahead of all other test cases in the suite, and its outcome (pass/fail) will decide whether the rest of the test cases should execute or not. Clear enough, no? Let me give you some real-project examples to understand the applications of sanity testing, and how it helps us to optimize the execution time which is the real meat of this article.
 
Demo Application
 
MorningSmoothie is a smoothie business application. For the sake of demo and handy understanding of the concept of Sanity Testing using TestNG, I have covered unit testing for only one feature of this application, which is product management. Here goes the project structure,
 
Learn About Sanity Testing With TestNG
 
The code can be found on my git. This is obviously a simple and standard structure for any Java testing framework. Two major folders, main and test extend from the src folder and contain further sub-packages. main contains codebase (application related classes, constants, business logic layer, utilities, etc.). Whereas, folder test is for the feature-specific test cases. For this sample, the resources folder in both, the main and test are empty. We store configuration files in here.
 
Look closer and you can see that I kept my POJO separated with my repository (ProductManager) class in main to augment its interactivity in an independent way. Similarly, in the test folder, there are dedicated packages for logically different classes.
 
However, this is my approach to logically design a small testing application. You can design your classes and their derivation in any way you want as long as they remain reusable and make the scalability of your framework easy.
 
Code Explanation
  1. // src/main/java/com/morningSmoothies/repositories/ProductManager.java  
  2. public class ProductManager {  
  3.     private List < FreshSmoothie > productStorage;  
  4.     public ProductManager() {  
  5.         productStorage = new ArrayList < FreshSmoothie > ();  
  6.     }  
  7.     // TODO: you can add any fancy business logic before performing any of the CRUD operations  
  8.     public boolean addProduct(FreshSmoothie smoothie) {  
  9.         return productStorage.add(smoothie);  
  10.     }  
  11.     public FreshSmoothie getProduct(int id) {  
  12.         for (FreshSmoothie s: productStorage) {  
  13.             if (s.equals(id)) {  
  14.                 return s;  
  15.             }  
  16.         }  
  17.         return null;  
  18.     }  
  19.     public boolean deleteProduct(final int id) {  
  20.         return productStorage.removeIf(e - > e.equals(id));  
  21.     }  
  22.     public List < FreshSmoothie > getAllProducts() {  
  23.         return productStorage;  
  24.     }  
  25. }  
Starting with the ProductManager class [CodeSample1]. We have some methods to perform different day to day business operations related to our product. These are simple, CRUD operations (Create, read, update, and delete) which consume our private productStorage. Now, we will write some unit tests to check if these functions are working as expected [CodeSample2].
  1. // src/test/java/unit/FreshSmoothie/CRUDTests.java  
  2. public class CRUDTests extends BaseClassUnitTesting {  
  3.     ProductManager pm;  
  4.     FreshSmoothie fs1, fs2, fs3;  
  5.     @BeforeMethod  
  6.     public void localSetup() {  
  7.         // Arrange setup  
  8.         fs1 = new FreshSmoothie(1, "MalonSmoothie", 25);  
  9.         fs2 = new FreshSmoothie(2, "PeachSmoothie", 30);  
  10.         fs3 = new FreshSmoothie(3, "MangoSmoothie", 35);  
  11.         pm = new ProductManager();  
  12.     }  
  13.     @Test(description = "Verify that addProduct method returns true  
  14.             when adds product successfully ")  
  15.             public void successfulProductAdditionReturnsTrue() {  
  16.                 // Act  
  17.                 boolean result = pm.addProduct(fs1);  
  18.                 // Assert  
  19.                 Assert.assertTrue(result);  
  20.             }  
  21.             @Test(description = "Verify that getProduct method returns null if product does not exist"public void nonExistingProductReturnsNull() {  
  22.                 // Arrange  
  23.                 boolean result = pm.addProduct(fs1);  
  24.                 // Act  
  25.                 FreshSmoothie fsReturned = pm.getProduct(fs1.getID());  
  26.                 // Assert  
  27.                 Assert.assertNull(fsReturned, "The method should return null  
  28.                     if it doesn 't find an added product for the given ID");  
  29.                 }  
  30.                 @Test(description = "Verify that getAllProducts method returns valid product collection")  
  31.                 public void productStorageReturnValidCount() {  
  32.                     // Arrange  
  33.                     pm.addProduct(fs1);  
  34.                     pm.addProduct(fs2);  
  35.                     pm.addProduct(fs3);  
  36.                     // Act  
  37.                     List < FreshSmoothie > smoothies = pm.getAllProducts();  
  38.                     // Assert  
  39.                     Assert.assertEquals(3, smoothies.size());  
  40.                 }  
  41.             }  
I cannot bring myself to explain the code line by line, as I think this is too explanatory. Therefore, I have arranged my tests in AAA notation (Arrange – Act – Assert). These tests are really simple ones, but do the job for the sake of the demo. One thing to note here is, we have a local setup which is to arrange mutual holders for all the tests in the class, take this as a local suite setup which has to run before every test in this class. Moreover, I have added GlobalSetup to mimic the behavior of the global suite setup which will run once before every execution of the suite. This global suite setup is added in the base class – BaseClassUnitTesting.
 
Obviously, you will not get the importance of these local and global suite and method level calls at this point, however, these are crucial in a real application to clear the mutual resources, creating metadata and initial sessions, or for the reporting logs to its least parts.
 
Noticeably, there are five tests in this test/unit folder with the @Test notation. I have written Sanity test cases in the test/unit/sanityTests folder; however, I have not added any group and DependsOn fields yet. So, we expect our test to run as normal in alphabetical order.
 
As a TestNG IntelliJ user, you must know multiple ways to run your tests in a class or package. So, I’m running them manually by selecting the following option on my test/unit folder.
 
Learn About Sanity Testing With TestNG
 
Once your tests get executed, you will see the below output window. By scanning the output window, you can see the sanity tests have been executed but in normal order. Nevertheless, the object is to run our sanity test before other tests and ensure the execution of those other tests based on the successful outcome of our sanity tests.
 
Learn About Sanity Testing With TestNG
 
So, I will just add fields groups and dependsOnGroups with the sanity and other CRUDTests class tests respectively.
 
You must be intrigued about the group with the name sanity. Refer to the screenshot below for this. These sanity tests were already part of our previous execution, but without the field @Test(groups = "sanity").
 
Learn About Sanity Testing With TestNG
 
Learn About Sanity Testing With TestNG
 
Now we will run our tests again as previously and note if there is any difference in the execution sequence.
 
Fair enough, right? As this time the test execution sequence is not alphabetical but logically dependent. Sanity tests ran first before the other tests. Even now, I am sure, most of you have not gotten the idea and benefit we can get just by making our test execution sequence dependent on some logic.
 
Therefore, for the demo, I will make my sanity test fail intentionally and then let you observe the difference.
 
Learn About Sanity Testing With TestNG
 
Please note the highlighted parts of the image. I have generated ‘Divided by Zero Exception’ to make my sanity test fail, and the execution of the dependent tests is ignored. In contrast, if there will be no such group and dependsOnGroups fields used in our tests, our execution time will be wasted if some very basic check which has to execute successfully were failed and make the execution of all other tests eventually fail too.
 

Conclusion

 
The article was meant to be a short guide to use TestNG features groups and dependsOnGroups to promote Sanity Testing. I walked you through the concept of how we should organize our tests in a logically dependent order so that they can optimize our execution time at times when very fundamental checks are failing.
 
The sanity test written for the demo application was covering the basic functionality which needed to perform well in order to pass all the other tests, such as the creation of product storage and a successful add operation for our product. And if any of this fails, execution of the rest of the tests is meaningless, as they will have to fail anyway. Lastly, the code sample is uploaded on the github which you can clone and start off right away.