function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Arturo AlamillaArturo Alamilla 

Batch Class Test to Production problem.

Hello everybody.

I'm a new salesforce developer, and i'm facin my first real world problem in my company.

We have a custom object named "Solicitudes__c", is the detail side of a master relationship of another custom object.

I was asked to calculate the time between stages (Picklist field named "Etapas_de_solicitud__c" with 10 values).
My solution is use History object records to calculate the first date to enter one stage, and the last day touching that same stage for every stage. I had to create some sample records and move them through all stages.

Later i came out with this apex class:
public with sharing class SolicitudesTiempos {

    public static void tiempos(List<Solicitudes__c> solicitudes) {
        
        //transform the List to Set of Ids
        set<id> solicitudesIds = new set<Id>();
        for(solicitudes__c s : solicitudes){
            solicitudesIds.add(s.id);
        }

        //list used to update the Solicitudes__c records lateer
        List<Solicitudes__c> solicitudesActualizar = new List<Solicitudes__c>();

        //gather all Solicitudes__History records.
        list<Solicitudes__History> historialBuscado = [SELECT ParentId, NewValue, OldValue, CreatedDate
                                                        FROM Solicitudes__History 
                                                        WHERE ParentId =: solicitudesIds
                                                        AND Field = 'Etapas_de_Solicitud__c'
                                                        WITH SECURITY_ENFORCED
                                                        ORDER BY CreatedDate ASC];
                                               
                                                        
        // lists to store the history changes, accordingly.
        list<Solicitudes__History> historialCotizacion = new list<Solicitudes__History>();
        list<Solicitudes__History> historialAceptacion = new list<Solicitudes__History>();
        list<Solicitudes__History> historialPreanalisis = new list<Solicitudes__History>();
        list<Solicitudes__History> historialDocumentos = new list<Solicitudes__History>();
        list<Solicitudes__History> historialCarga = new list<Solicitudes__History>();
        list<Solicitudes__History> historialPendienteAnalizar = new list<Solicitudes__History>();
        list<Solicitudes__History> historialComite = new list<Solicitudes__History>();
        list<Solicitudes__History> historialCondicionada = new list<Solicitudes__History>();
        list<Solicitudes__History> historialAutorizada = new list<Solicitudes__History>();
        list<Solicitudes__History> historialPendientedeFirma = new list<Solicitudes__History>();


        // loop throughout all solicitudes__history to store the appropiate history record to a list.
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Cotización' || s.OldValue == 'Cotización'){
                historialCotizacion.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Aceptación' || s.OldValue == 'Aceptación'){
                historialAceptacion.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Preanálisis' || s.OldValue == 'Preanálisis'){
                historialPreanalisis.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Documentos' || s.OldValue == 'Documentos'){
                historialDocumentos.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Aviso a sebas' || s.OldValue == 'Aviso a sebas'){
                historialCarga.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'pendiente de analizar' || s.OldValue == 'pendiente de analizar'){
                historialPendienteAnalizar.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'comite' || s.OldValue == 'comite'){
                historialComite.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'condicionado' || s.OldValue == 'condicionado'){
                historialCondicionada.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'autorizada' || s.OldValue == 'autorizada'){
                historialAutorizada.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'pendiente de firma' || s.OldValue == 'pendiente de firma'){
                historialPendientedeFirma.add(s);
            }
        }

        // loop to calcule every Solicitudes__c record time on each stage.
        for(Id s : solicitudesIds){

            Decimal tiempoCotizacion = SolicitudesTiempos.calcularTiempo(historialCotizacion, s);
            Decimal tiempoAceptacion = SolicitudesTiempos.calcularTiempo(historialAceptacion, s);
            Decimal tiempoPreanalisis = SolicitudesTiempos.calcularTiempo(historialPreanalisis, s);
            Decimal tiempoDocumentos = SolicitudesTiempos.calcularTiempo(historialDocumentos, s);
            Decimal tiempoCarga = SolicitudesTiempos.calcularTiempo(historialCarga, s);
            Decimal tiempoPendienteAnalizar = SolicitudesTiempos.calcularTiempo(historialPendienteAnalizar, s);
            Decimal tiempoComite = SolicitudesTiempos.calcularTiempo(historialComite, s);
            Decimal tiempoCondicionado = SolicitudesTiempos.calcularTiempo(historialCondicionada, s);
            Decimal tiempoAutorizada = SolicitudesTiempos.calcularTiempo(historialAutorizada, s);
            Decimal tiempoPendienteFirma = SolicitudesTiempos.calcularTiempo(historialPendientedeFirma, s);

            //store each calculation to the record's field and add it to a collection to update later.
            Solicitudes__c solicitud = new Solicitudes__c(Id = s, 
                                                            Tiempo_cotizacion__c=tiempoCotizacion,
                                                            Tiempo_Aceptacion__c=tiempoAceptacion,
                                                            Tiempo_preanalisis__c=tiempoPreanalisis,
                                                            Tiempo_Documentos__c=tiempoDocumentos,
                                                            Tiempo_Carga__c=tiempoCarga,
                                                            Tiempo_Por_analizar__c=tiempoPendienteAnalizar,
                                                            Tiempo_Comite__c=tiempoComite,
                                                            Tiempo_Condicionada__c=tiempoCondicionado,
                                                            Tiempo_Autorizada__c=tiempoAutorizada,
                                                            Tiempo_Pendiente_Firma__c=tiempoPendienteFirma);
            
            //add the new record to a collection to update later.
            solicitudesActualizar.add(solicitud);
        }
        
        //update the records
        update solicitudesActualizar;
    }


    //method to calculate time between stages.
    private static Decimal calcularTiempo (list<Solicitudes__History> historial, Id solicitudId){

        if(historial.size()>0 && historial != null){
            dateTime date1;
            dateTime date2;
            Decimal minutos;
            for(Solicitudes__History hist : historial){
                if(hist.ParentId == solicitudId && date1 == null){
                    date1 = hist.CreatedDate;
                }else if(hist.ParentId == solicitudId && date1 != null){
                    date2 = hist.CreatedDate;
                }
            }
            if(date1 != null && date2 != null){
                Long firstDate = date1.getTime();
                Long finalDate = date2.getTime();
                Decimal segundos = (finalDate-firstDate)/1000;
                minutos = segundos/60;
                return minutos;
            }else{
                return 0.00;
            }
        }else{
            return 0.00;
        }   
    }
}

Then i came out with an batch apex class:
public class SolicitudesTiemposBatch implements Database.Batchable<SObject> {

    public Database.QueryLocator start(Database.BatchableContext bc) {
        String query = 'SELECT Id FROM Solicitudes__c '+
                'WHERE CreatedDate >= 2022-01-01T00:00:00Z '+
                'ORDER BY CreatedDate ASC';
                return DataBase.getQueryLocator(query);
    }

    public void execute(Database.BatchableContext bc, List<Solicitudes__c> scope){
        SolicitudesTiempos.tiempos(scope);
    }

    public void finish(database.BatchableContext bc){
    }
}

And create a Test class:
@isTest
public class SolicitudesTiemposBatchTest {
    @isTest(SeeAllData=true)
        static void test(){

            Test.startTest();
            SolicitudesTiemposBatch solicitudesBatch = new SolicitudesTiemposBatch();
            Database.executeBatch(solicitudesBatch, 200);
            Test.stopTest();
            
            System.assertEquals(199,[SELECT count() FROM Solicitudes__C], '199 solicitudes');
        }
}
And i get enough coverage to deploy production.
Code coverage

The problem is that when i'm trying to validate in production i see this problem: "System.UnexpectedException: No more than one executeBatch can be called from within a test method. Please make sure the iterable returned from your start method matches the batch size, resulting in one executeBatch invocation.
Stack Trace: External entry point"

Any suggestions?
Best Answer chosen by Arturo Alamilla
Gian Piere VallejosGian Piere Vallejos
You are using "@isTest(SeeAllData=true)", that means that you are using the real data from your organization. That's not a good practice to develop tests. I think your organization have more than one batch running at the same time and it throws the exception.

You need to use only @isTest and mock your data. That means you need to create a loop that generates the 199 solicitudes and his relationships. 

Maybe this article can help you: https://jayakrishnasfdc.wordpress.com/2021/01/02/apex-test-class-for-batch-apex/

I hope this help you. In addition, let me know if you speak spanish to give more detail in that language.

All the best for you! 

 

All Answers

Gian Piere VallejosGian Piere Vallejos
You are using "@isTest(SeeAllData=true)", that means that you are using the real data from your organization. That's not a good practice to develop tests. I think your organization have more than one batch running at the same time and it throws the exception.

You need to use only @isTest and mock your data. That means you need to create a loop that generates the 199 solicitudes and his relationships. 

Maybe this article can help you: https://jayakrishnasfdc.wordpress.com/2021/01/02/apex-test-class-for-batch-apex/

I hope this help you. In addition, let me know if you speak spanish to give more detail in that language.

All the best for you! 

 
This was selected as the best answer
Arturo AlamillaArturo Alamilla
Hello, the problem is that my class relies on Solicitudes__History data, and what i search online is that you cannot create History records because OldValue, NewValue are not writable. 

And yes, i do speak spanish.