Monday, September 12, 2016

Testing the Standard Controller

Introduction


Testing Standard Controllers is quite easy and straight forward and in this post I will show you how. For more information about Standard Controllers please read Visualforce Controllers - stay standard & extend? or go wild

Let's begin by looking at the Apex class and Visualforce we want to write a test for.

Apex Class - FinancialStatementControllerExt.cls


 public with sharing class FinancialStatementControllerExt {  
   public Financial_Statement__c financialStatement { get; set; }  
   public ApexPages.StandardController stdController { get; set; }  
   public FinancialStatementControllerExt(ApexPages.StandardController stdController){  
     this.financialStatement = (Financial_Statement__c)stdController.getRecord();  
     this.stdController = stdController;  
   }  
   public PageReference saveFinancialStatement(){  
     try {  
       if(this.financialStatement.Account__c == null)  
         this.financialStatement.Account__c = this.getDefaultAccountId();  
       insert this.financialStatement;  
       return new PageReference('/' + this.financialStatement.Id);  
     } catch(Exception e) {  
       ApexPages.addMessage(new ApexPages.Message(  
         ApexPages.Severity.Error,   
         'An error occurred while inserted a new financial statement record. Please contact your admin!')  
       );  
     }  
     return null;  
   }  
   public PageReference cancelOperation(){  
     return this.stdController.cancel();  
   }  
   private Id getDefaultAccountId(){  
     try {  
       return [SELECT Id FROM Account WHERE Name = 'Default Account' LIMIT 1].Id;  
     } catch (Exception e){  
       ApexPages.addMessage(new ApexPages.Message(  
         ApexPages.Severity.Error,   
         'An error occurred retrieving the default Account. Please contact your admin!')  
       );  
     }  
     return null;  
   }  
 }  


Visualforce - FinancialStatement.page



 <apex:page standardController="Financial_Statement__c"   
      extensions="FinancialStatementControllerExt" showHeader="true" sidebar="true">  
      <apex:form>  
           <apex:sectionHeader title="Create Financial Statement"   
                subtitle="New Financial Statement" rendered="{!Financial_Statement__c.Id == null}" />  
           <apex:sectionHeader title="Update Financial Statement"   
                subtitle="{!Financial_Statement__c.Name}" rendered="{!Financial_Statement__c.Id != null}" />  
           <apex:pageBlock title="Financial Statement">  
                <apex:pageBlockButtons location="top">  
                     <apex:commandButton value="Save" action="{!saveFinancialStatement}" />  
                     <apex:commandButton value="Cancel" action="{!cancelOperation}" />  
                </apex:pageBlockButtons>  
                <apex:pageBlockSection title="Enter a new Financial Statement">  
                     <apex:inputField value="{!Financial_Statement__c.Name}" />  
                     <apex:inputField value="{!Financial_Statement__c.Account__c}" />  
                </apex:pageBlockSection>  
           </apex:pageBlock>  
      </apex:form>  
 </apex:page>  


Test Class - FinancialStatementControllerExtTest.cls


So looking at the Visualforce and  Controller Extension above, what would you expect your test class to have?

  • The extension uses a Standard Controller for "Financial_Statement__c", so we will have to create that Standard Controller in the Test as well.
  • The extension makes use of a default account, so we need to create a default account
  • This also means we need to create at least two test cases, one for when a user enters an account and one for when the extension uses the default account
  • There are two try/catch blocks in the extension, one for the DML insert of a new financial statement and one for retrieving the default account. This means we have to write at least 2 negative test cases
  • There is a cancel operation going on in the controller as well. This means we need a test case for this operation too
  • And since we are testing a Visualforce page, we will need to reference this page in our test code
So I think that should be it. I haven't covered all these test cases in my test class. I have left out the negative test case for the insert of the financial statement and I haven't tested the cancel operation. Also the second positive test case where a user supplies the account to use for the financial statement is not covered. So 3 test methods are not written. This will be a nice exercise for you. Remember practice makes perfect. Currently the class has a coverage of 73%. I challenge you to raise it to 100% and add your additional test use cases to the comment.

So my test class looks like this;


 @isTest(SeeAllData=false)  
 private class FinancialStatementControllerExtTest {  
      private static User testUser;  
      private static Account defaultAcct;  
      private static Financial_Statement__c financialStatement;  
      private static ApexPages.StandardController stdController;  
      static {  
           testUser = TestDataFactory.createActiveUser();   
           /* Insert an account with the name 'Default Account' */  
           TestDataFactory.setFieldNameValuePairs('Name', 'Default Account');  
           defaultAcct = (Account)TestDataFactory.createSObject('Account');  
           //insert defaultAcct;  
           TestDataFactory.resetFieldNameValuePairs();  
           /* Create a financial statement and it's standard controller. Do not insert it yet */  
           financialStatement = new Financial_Statement__c();  
           stdController = new ApexPages.StandardController(financialStatement);  
      }  
      /* Inserting with no account, should create the Financial Statement with a default account */  
      @isTest static void test_insertWithNoAccount() {  
           insert defaultAcct;  
           FinancialStatementControllerExt fsControllerExt;  
           PageReference pageRef;  
           PageReference returnPage;  
           Test.startTest();  
                System.runAs(testUser){  
                     pageRef = Page.FinancialStatement;  
                     Test.setCurrentPage(pageRef);  
                     fsControllerExt = new FinancialStatementControllerExt(stdController);  
                     returnPage = fsControllerExt.saveFinancialStatement();  
                }  
           Test.stopTest();  
           Financial_Statement__c insertedFinancialStatement = [  
                SELECT Id, Name, Account__c   
                FROM Financial_Statement__c   
                WHERE Id = :fsControllerExt.financialStatement.Id LIMIT 1  
           ];  
           System.assertNotEquals(null, returnPage, 'Pagereference is null, therefore Financial Statement was not created');  
           System.assertEquals('/' + insertedFinancialStatement.Id, returnPage.getUrl(),   
                'Pagereference return url is wrong, therefore Financial Statement was not created');  
           System.assertEquals(defaultAcct.Id, insertedFinancialStatement.Account__c,   
                'The default Acct was not attached to the inserted financial statement');  
      }  
      @isTest static void test_insertNegativeTestCase() {  
           FinancialStatementControllerExt fsControllerExt;  
           PageReference pageRef;  
           PageReference returnPage;  
           Boolean errorOccurred = false;  
           Test.startTest();  
                System.runAs(testUser){  
                     pageRef = Page.FinancialStatement;  
                     Test.setCurrentPage(pageRef);  
                     fsControllerExt = new FinancialStatementControllerExt(stdController);  
                     returnPage = fsControllerExt.saveFinancialStatement();  
                }  
           Test.stopTest();  
           List<ApexPages.Message> msgs = ApexPages.getMessages();  
           for(ApexPages.Message msg : msgs){  
                if(msg.getDetail().contains('An error occurred retrieving the default Account. Please contact your admin!'))  
                errorOccurred = true;  
           }  
           Financial_Statement__c insertedFinancialStatement = [  
                SELECT Id, Name, Account__c   
                FROM Financial_Statement__c   
                WHERE Id = :fsControllerExt.financialStatement.Id LIMIT 1  
           ];  
           System.assert(errorOccurred,   
                'Due to the missing default account, an exception should have be caught and handled. This is not the case!');  
           System.assertEquals(null, insertedFinancialStatement.Account__c,   
                'The Account__c should be null because no default account was created');  
      }  
 }  


Notice that I use a TestDataFactory to create my test data. How to create such a test data factory, is a topic for an entire blog that I will tackle in the future.

First create the test data that you need, which in this case is a test user, a default account, a financial statement and a Standard Controller for the financial statement. The default account and financial statements are not inserted yet. The financial statement will be inserted as part of the test.

The first test case test_insertWithNoAccount begins by inserting the default account so that it is available to the controller extension when creating the account. This first step is missing from the negative test case and causes the exception in the getDefaultAccountId() in the controller exception to be thrown

For both test cases, we set the current page to FinancialStatement which is the page we are testing. We then create an instance of the controller extension by calling it's constructor and passing it the Standard Controller for the financial statement record.

In the test_insertWithNoAccount test case, we call the saveFinancialStatement() method and then assert that the page will be properly redirected to the detail page of the newly created financial statement. We also assert that the default account was attached to the financial statement.

In the negative test case test_insertNegativeTestCase we assert that the Financial Statement was created with no account because a default account does not exist. We also assert that the right error message was added to the page.

Best Practices


  1. Always create test data for your test. Tests should not depend on the environment in which they run, period.
  2. Your test cases should be complete and independent of other test cases. Concretely the results on one test should not depend on the results of another test
  3. Write positive as well as negative test. Depending on how tricky your use case it, writing test classes to assert negative or unwanted behaviour can be challenging and many developers just comfortably skip them, BAD
  4. Use Test.start() and Test.stop() methods to run your test. Code between these two methods runs with governor limits reset. So you get a clearer picture of how your code is doing against those nasty little limits
  5. Always use System.runAs() to test your code within the context of a specific user. This is especially important for security and data access test as you can confirm that Users have access to the data they need and others don't have access to data they do not need
  6. This is the last and most important one and if you haven't been doing this and do not change immediately, you have no business writing Apex code. ASSERT expected behaviour. Very important. Assert the logic in your code and do not write test classes just to gain coverage. Way too often, I have seen controller test classes that have a single method which just calls all the methods in the controller without a single assert. If you keep doing this, one day the Lord of Apex might strike you down. NO  guys, on a serious note, make it a habit to assert your code. It might one day literally save your life by helping you catch some very nasty bugs that would otherwise be very costly

Conclusion


We have looked at the best way to test Standard controllers and have discussed aspects that will normally affect such test. We concluded by looking at best practices that I have also included in my code example. They apply to writing test code in general and not just test for Standard controllers. Please take these best practices to heart, own them and live them. Practice makes perfect. Also try to complete the test class above and raise it's coverage to 100%. Reach out if you need my assistance.

As usual it is always a pleasure to share my knowledge and I appreciate your feedback, questions and critique

No comments:

Post a Comment