apex-enterprise-patterns / fflib-apex-common-samplecode

Samples application illustrating the Apex Enterprise Patterns library
BSD 3-Clause "New" or "Revised" License
205 stars 105 forks source link

Hello World Tutorial for APEX Mocks #10

Open mhaseebkhan opened 8 years ago

mhaseebkhan commented 8 years ago

I am working on a project in which we need to test an APEX REST Service in such a way that no actual data is created in Salesforce during the testcases execution. I am new to APEX Development.

Searching on the web took me to fflib-apex-mocks library which seems to serve the purpose of avoiding the usage of actual test data inserted in the database for testcases execution. I have installed fflib-apex-mocks and fflib-apex-common in my sandbox environment.

So far, I am not able to find out a step-by-step guide as what I need to do if I want to test my existing Salesforce Objects at a unit level and how can I test my APEX Rest Service? From looking at the fflib-apex-common-samplecode it seems that I need to define classes for my Salesforce objects, I am not sure if I am getting this right.

At a higher-level, below are my questions at a higher-level:

Can someone please help me out in getting started?

afawcett commented 8 years ago

So ApexMocks and Apex Enterprise Patterns have some defined points where mocks can be injected to allow you to test your class code in isolation without needing to setup things dependent classes require. I cover this in my Dreamfroce 2015 talk, recorded here. There is also a two part blog series, here and here.

In your case I'd expect you to have the following...

Traditional Apex Tests are Integration Tests

You will need at least one integration unit test for your trigger and your REST API entry points. These test the full stack, needed for at least for the one line Trigger coverage if nothing else. Basically these are your traditional apex test that inserts some objects, asserts the domain / trigger code did its thing. And also one that inserts some records, calls the Apex REST class method (emulating the URL parameters) and asserts it returns the correct records.

Ok, but lets get writing some more true Unit Tests

So what we want to do is write some more varied and more finely targeted unit test methods. In your case the following and mocking accordingly...

Enabling Dependency Injection

The blogs above show how to generate the mock classes for Selectors, Domain and Service classes and how to use them in your code (via the Application factory class) to facilitate runtime mocking. The blogs also show how to inject mock versions of each according to what test from above your writing.

For example when unit testing your Apex Rest class code, you would want to mock the Service layer calls. This example while its a controller class test (imagine its your rest apex class) shows how this works.

Other thoughts...

The blog links above should give a good start in terms of how to setup your classes and interfaces. I do think though you raise a good point and something i'd like to keep this issue open to represent the need for a more step by step list for sure.

mhaseebkhan commented 8 years ago

Thanks, @afawcett for your input. I posted similar question on StackExchange which was answered by @frup42 previously.

Initially, my code didn't had any layers but now I have added a Service Layer which is responsible for executing DML, however, I am facing problem while Mocking it.

As a sample, I have provided a snippet below to explain the scenario.

// Service class
global with sharing class TeacherService {
  global static Teacher[] getAllTeachers() {
    Teacher[] teachers = [Select Id, Name From Teacher];
    return teachers;
  }
}

// Testing code
fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_ISObjectUnitOfWork uowMock = new fflib_SObjectMocks.SObjectUnitOfWork(mocks);
TeacherService serviceMock = new Mocks.TeacherService(mocks);

// Below is the error on line 3 of the testing code above.
// Invalid type: Mocks.TeacherService

Can you please have a look and suggest if I am doing something wrong? I have tried to follow fflib-apex-common-samplecode to the best of my understanding.

Looking forward to hear from you on this soon.

afawcett commented 8 years ago

Excellent, i've up-voted both question and answer! :+1:

Your service is missing the Apex interface and the implementation of it that allows ApexMocks to do its thing. The missing class is created by the Apex Mocks Generator. If you take a look at Part 1 of the blog i referenced above there is an example of how to setup, register and utilise your service code from your Apex REST class method, to make this work. Part 2 talk about configuring and running the Apex Mocks Generator.

If your testing your Apex Rest Class code, the only thing you need to mock is the Service. You don't need to mock the Unit Of Work, as that is not a dependency of your Apex Rest Class code. However once you get to writing your test code for the Service itself you will want to mock the Unit of Work (assuming your using the Unit Of Work, your example above is just a query) and/or Selector dependencies (again only if you have any).

You will need....

Again the blogs I've referenced above show how to setup the above classes. And critically run the Apex Mocks Generator tool, to generate the Mocks.TeacherService mock class. Its also worth noting that typically you would extract your SOQL code into a Selector, but lets leave that for now, once you have this up and running you'll see how to do it for Selector and Domain layers.

afawcett commented 8 years ago

ITeachersService.cls

public interface ITeachersService
{
    Teacher__c[] getAllTeachers();
}

TeachersService.cls

global with sharing class TeachersService
{
    global static Teacher__c[] getAllTeachers()
    {
        return service().getAllTeachers();
    }

    private static ITeachersService service()
    {
        return (ITeachersService) Application.Service.newInstance(ITeachersService.class);
    }
}

TeachersServiceImpl.cls

public class TeachersServiceImpl
    implements ITeachersService
{
    public Teacher__c[] getAllTeachers() {
        Teacher__c[] teachers = [Select Id, Name From Teacher__c];
        return teachers;
    }
}

**Application.cls***

public class Application
{
    // Configure and create the ServiceFactory for this Application
    public static final fflib_Application.ServiceFactory Service =
        new fflib_Application.ServiceFactory(
            new Map<Type, Type> {
                    ITeachersService.class => TeachersServiceImpl.class });
}

TeachersResource.cls

@RestResource(urlMapping='/Teachers/*')
global class TeachersResource 
{
    @HttpGet
    global static Teacher__c[] getAllTeachers() 
    {
        return TeachersService.getAllTeachers();
    }
}

TeachersResoruceTest.cls

@IsTest 
private class TeachersResourceTest 
{
    private void callingRestMethodCallServiceWhenInvoked()
    {
        fflib_ApexMocks mocks = new fflib_ApexMocks();

       // Given
       mocks.startStubbing();
       Mocks.TeachersService mockService = new Mocks.TeachersService(mocks);
       mocks.when(mockService.getAllTeachers()).thenReturn(
            new List<Teacher__c> { new Teacher__c( Name = 'Fred')}); // Mock service response
       mocks.stopStubbing();

       // When 
       List<Teacher__c> teachers = TeachersResource.getAllTeachers(); // (HTTP GET on /Teachers)

       // Then 
       (IOpportunitiesService) mocks.verify(mockService, 1)).getAllTeachers(); // Service called?
       System.assertEquals('Fred', teachers[0].Name);  // Mocked Service response returned?
    }
}

Some Notes:

mhaseebkhan commented 8 years ago

Thanks for your input and directions, @afawcett. It has been certainly very helpful. The file extension I have is apxc and not cls, but, I am not sure if that can cause a problem.

Also, please provide your input on the following:

TeachersService.apxc

global with sharing class TeachersService
{
    global static Teacher__c[] getAllTeachers() {
        return service().getAllTeachers();
    }

    private static ITeachersService service() {
        fflib_Application.ServiceFactory Service = new fflib_Application.ServiceFactory(
            new Map<Type, Type> {
                ITeachersService.class => TeachersServiceImpl.class
            }
        );
        return (ITeachersService) Service.newInstance(TeachersServiceImpl.class);
    }
}

Mocks.apxc

@isTest
public class Mocks {
    public class TeachersService {
        private fflib_ApexMocks mocks;

        public TeachersService(fflib_ApexMocks mocks) {
            this.mocks = mocks;
        }

        public Teacher__c[] getAllTeachers() {
            return (Teacher__c[]) mocks.mockNonVoidMethod(this, 'getAllTeachers', new List<Type>(), new List<Object>());
        }
    }
}

Looking forward to your input.

afawcett commented 8 years ago

You need to have the ServiceFactory held at request scope, e.g. in a static some place, it does not need to be in Application class, this is just a naming convention. Basically it needs to be static, because you will have no way to register your Mock instance before invoking the code you want to test.

I'm not 100% sure what you mean by both the actual and mock methods gets called from my REST service tbh. Looking at this code above it should always use the none mock route as you have not provided anyway to register the mock implementation, though i've not seen your test code so cannot comment entirely on this. Can you share that?

mhaseebkhan commented 8 years ago

Thanks for the reply, @afawcett. Please find below the code.

ITeachersService.cls

public interface ITeachersService {
    Teacher__c[] getAllTeachers();
}

TeachersServiceImpl.cls

public class TeachersServiceImpl implements ITeachersService {
    public Teacher__c[] getAllTeachers() {
        Teacher__c[] teachers = [Select Id, Name From Teacher__c];
        return teachers;
    }
}

TeachersService.cls

global with sharing class TeachersService {
    global static Teacher__c[] getAllTeachers() {
        return service().getAllTeachers();
    }

    private static ITeachersService service() {
        fflib_Application.ServiceFactory Service = new fflib_Application.ServiceFactory(
                new Map<Type, Type>{
                        ITeachersService.class => TeachersServiceImpl.class
                }
        );
        return (ITeachersService) Service.newInstance(TeachersServiceImpl.class);
    }
}

TeachersResource.cls

@RestResource(URLMapping='/Teachers')
public with sharing class TeachersResource {
    @HttpGet
    global static Teacher[] getTeachers() {
        return TeachersService.getAllTeachers();

    }
}

TeachersResourceTest.cls

@IsTest
private class TeachersResourceTest {
    private void callingRestMethodCallServiceWhenInvoked() {
        fflib_ApexMocks mocks = new fflib_ApexMocks();

        // Given
        mocks.startStubbing();
        Mocks.TeachersService mockService = new Mocks.TeachersService(mocks);
        mocks.when(mockService.getAllTeachers()).thenReturn(
                new List<Teacher__c>{
                        new Teacher__c(Name = 'Fred')
                }
        );
        mocks.stopStubbing();

        // When
        List<Teacher__c> teachers = TeachersResource.getAllTeachers();

        // Then
        ((ITeachersService) mocks.verify(mockService, 1)).getAllTeachers();
        System.assertEquals('Fred', teachers[0].Name);
    }

    private class MockTeachersService implements TeachersService.ITeachersService {
        private fflib_ApexMocks mocks;

        public MockTeachersService(fflib_ApexMocks mocks) {
            this.mocks = mocks;
        }
    }
}

The APEX Mocks generator inserted the private class MockTeachersService implements TeachersService.ITeachersService piece in TeachersResourceTest.cls. I am not sure if the class is correct because its named as MockTeachersService but it should be named as TeachersService as thats what is used in TeachersResourceTest. Also MockTeachersService doesn't have a getAllTeachers method, so, I added it manually.

By both actual and mock methods I mean is that when I execute the testcase, the getAllTeachers method in MockTeachersService and TeachersService gets called. I checked that with putting some System.debug statements.

Kindly suggest what changes do I need to do.

afawcett commented 8 years ago

Ah sorry, my bad, the key thing is missing, setting the mock instance!

Application.Service.setMock(ITeachersService.class, mockService);
jinlii commented 3 years ago

public class Application { // Configure and create the ServiceFactory for this Application public static final fflib_Application.ServiceFactory Service = new fflib_Application.ServiceFactory( new Map<Type, Type> { ITeachersService.class => TeachersServiceImpl.class }); }

public interface ITeachersService { Teacher__c[] getAllTeachers(); }

global with sharing class TeachersService { global static Teacher__c[] getAllTeachers() { return service().getAllTeachers(); }

private static ITeachersService service()
{
    return (ITeachersService) Application.Service.newInstance(ITeachersService.class);
}

}

public class TeachersServiceImpl implements ITeachersService { public Teacherc[] getAllTeachers() { Teacherc[] teachers = [Select Id, Name From Teacher__c]; return teachers; } }

@RestResource(urlMapping='/Teachers/*') global class TeachersResource { @HttpGet global static Teacher__c[] getAllTeachers() { return TeachersService.getAllTeachers(); } }

@IsTest private class TeachersResourceTest { @istest private static void callingRestMethodCallServiceWhenInvoked() { fflib_ApexMocks mocks = new fflib_ApexMocks();

    // Given
    mocks.startStubbing();
    Mocks.TeachersService mockService = new Mocks.TeachersService(mocks);
    Application.Service.setMock(ITeachersService.class, mockService);
    mocks.when(mockService.getAllTeachers()).thenReturn(
        new List<Teacher__c>{
            new Teacher__c(Name = 'Fred')
                }
    );
    mocks.stopStubbing();

    // When
    List<Teacher__c> teachers = TeachersResource.getAllTeachers();

    // Then
    ((ITeachersService) mocks.verify(mockService, 1)).getAllTeachers();
    System.assertEquals('Fred', teachers[0].Name);
}

}

@isTest public class Mocks { public class TeachersService { private fflib_ApexMocks mocks;

    public TeachersService(fflib_ApexMocks mocks) {
        this.mocks = mocks;
    }

    public Teacher__c[] getAllTeachers() {
        return (Teacher__c[]) mocks.mockNonVoidMethod(this, 'getAllTeachers', new List<Type>(), new List<Object>());
    }
}

}

When I ran TeachersResourceTest, I got this error: System.TypeException: Invalid conversion from runtime type Mocks.TeachersService to ITeachersService. Can you please advise?

stohn777 commented 3 years ago

Hi @jinlii

(1) Although it's evident that you're mocking the service implementation in the test, the test-sphere naming is a bit confusing. (2) The mock should implement the ITeachersService interface too.

@isTest
public class Mocks {
    public class TeachersService implements ITeachersService {
        ...
    }
}