0

I'd like to preface this by saying that I'm very new to writing Apex test Classes, and Apex code in general. I found an Apex class from another post here, and slightly tweaked it for my needs. I've only ever written a single Apex trigger and very simple test class once before.

In summary, the Class takes the Data Category Group, and Data Categories of a Knowledge Article, and places those values into a custom text field on the Knowledge article itself. This is so we can expose them on a page layout.

Code here:

public class ArticleActionDataCategoryUpdate {
    // Invoked by process builder
    @InvocableMethod
    public static void updateArticleTypeForDataCategories(List<Id> articleTypeIds) 
    {
        List<Knowledge__kav> lstArticleType = [SELECT Id, Title, Topics__c FROM Knowledge__kav WHERE Id IN:articleTypeIds 
                                                    AND PublishStatus = 'draft'];

        List<Knowledge__DataCategorySelection> lstDC = [SELECT ParentId, DataCategoryGroupName, DataCategoryName   FROM Knowledge__DataCategorySelection
                                                            WHERE ParentId IN:articleTypeIds];

        Map<Id, String> datacategoryNameMap = new Map<Id, String>();
        Map<Id, String> datacategoryParentMap = new Map<Id, String>();

        for(Knowledge__DataCategorySelection dcObj:lstDC)
        {   
            if(datacategoryNameMap.containsKey(dcObj.ParentId))
            {
                String str =  datacategoryNameMap.get(dcObj.ParentId);
                datacategoryNameMap.put(dcObj.ParentId, str + ', ' + dcObj.DataCategoryName);
                datacategoryParentMap.put(dcObj.ParentId, dcObj.DataCategoryGroupName);
            }
            else
            {
               datacategoryNameMap.put(dcObj.ParentId,dcObj.DataCategoryName);
               datacategoryParentMap.put(dcObj.ParentId,dcObj.DataCategoryGroupName);
            }
        }

        for(Knowledge__kav artObj:lstArticleType)
        {
            if(datacategoryNameMap.containsKey(artObj.Id))
            {
                artObj.Topics__c = datacategoryParentMap.get(artObj.Id)  +', '+datacategoryNameMap.get(artObj.Id);
            }
        }
        update lstArticleType;
    }    
}

Test Class:

@isTest
public class TestArticleActionDataCategoryUpdate {
    @isTest
    public static void testUpdate(){
        Knowledge__kav kba = new Knowledge__kav(Title='title1',urlName='test-article');
        Knowledge__kav kvb = new Knowledge__kav();
        Knowledge__DataCategorySelection dcs = new Knowledge__DataCategorySelection(DataCategoryName='testcat');
        dcs.ParentId = kba.Id;
        Database.SaveResult res = Database.insert(kba,false);
        Database.SaveResult res2 = Database.insert(dcs,false);
        Knowledge__DataCategorySelection dcs2 = new Knowledge__DataCategorySelection(ParentId=kba.Id,DataCategoryGroupName='Parent');

       
        
        List<id> idList = new List<id>();
        idList.add(kba.id);

      
        ArticleActionDataCategoryUpdate.updateArticleTypeForDataCategories(idList);
        
        system.assertEquals(kba.Topics__c, kvb.Topics__c);
        system.assertEquals('testcat',dcs.DataCategoryName);
        system.assertequals('testcat',kba.Topics__c);
        system.assertEquals(kba.Topics__c, dcs.DataCategoryGroupName);
    }

}

My test class only gives me 47% coverage.

Any guidance or help here, as this is holding up production for us. Thanks

1 Answer 1

1

The golden rule of unit testing is you only get coverage for code that is executed as part of a unit test.

With unit testing, you are the one responsible for making sure that the correct data is in place to get you to execute the code path that you want coverage for. In many cases, you'll want to have multiple test methods. One test can create test data that causes you to execute the if block of an if-else while another test can create test data that causes you to execute the else block.

If you're running tests through the salesforce CLI (sfdx force:apex:test:run), you should be getting a list of un-covered lines of code. If you're running tests through the developer console, there's a coverage report in the upper-left part of the window (under the menus, above the code window) when you open a class in the developer console which gives you a visual indication of which lines aren't covered (they're highlighted in red).

Once you know which lines are un-covered, you can start working towards fixing that. Perhaps you need to tweak the test data that you're inserting; or perhaps its an indication that you should write another test method.

With such a short class though, it's fairly easy for experienced eyes to guess at what's wrong.

What I think is uncovered

I think there is one un-covered section:

for(Knowledge__DataCategorySelection dcObj:lstDC){
    <code omitted>
}

The for loop only runs if the query for data categories returns rows.

In your test method, you're using database.insert(List<SObject> records, Boolean allOrNone);. Setting allOrNone to false means that you aren't seeing an error that would otherwise be brought to your attention. It's allowing your test to continue running, but that's not what you want to have happen.

Tests are meant to tell you when there's a problem. If your code is failing or throwing an exception, you want to know about it.

I'd remove those database.insert lines, and just use the regular DML statements insert kba; and insert dcs; (you don't get saveresults from normal DML statements). After that, I suspect you'll need to make some adjustments to your data category to actually get it to be able to be inserted. Whatever error message you get should help you move forward.

Other observations

A given article must have multiple data categories for you to execute (and thus gain coverage for) the if block in your for loop over data categories. You shouldn't need to have coverage for that block to get past the 75% minimum, but it's a simple enough case that I'd argue that you should get coverage for it anyway.

That would be a good candidate for an additional test method in your test class.

Generally speaking, you want each test method to cover a single scenario (what happens if this query returns no results? what happens if field X is set to Y? what happens if field X is set to Z?). Testing many different scenarios (including "negative cases", where the input to the code you're testing is purposefully erroneous in one way or another) helps to ensure that your code is robust. Testing a sufficient number of scenarios will also naturally lead to high code coverage.

3
  • --Thanks for your help here. Provided a lot of good insight. By just fixing the data category Insert issues, I was able to achieve 84% coverage. Because I had all-or-nothing set to false, I wasn't seeing that error (I'm mostly parroting what I read--still very green) Any idea how I can cover for the if() under the for(). Even though I assigned the Data category Parent ID to the ID of the KB article, it still isn't picking it up via the method call, but I know it works. I've seen it in action in Dev Org and Sandbox. Maybe my references are wrong? May 1, 2021 at 22:57
  • @tontoandbandit if your test method still has a similar structure to what you initially posted here, then you never actually inserted the second data category dcs2 (meaning it won't be available to query in your test).
    – Derek F
    May 2, 2021 at 1:26
  • Your guidance really saved me here. The reason my Articles weren't picking up the properly referenced cat IDs, was because I was never re-querying the DB after they were inserted.... they were referencing old data, and the wrong ID (Id, vs KnowledgeArticleID) Second, you're right, I didn't commit dcs2. After doing that, it met the first if() condition. I now have 100% coverage. Thank you VERY MUCH for the help May 2, 2021 at 1:41

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .