Spec file calling real service despite having a mock spy implementation

36 views Asked by At

I am new to testing. Can't seem to get my spec file to call the mocked version of my service dependency during a test. Instead, it calls the original, which doesnt work because it's an API that only runs on a native device.

Function in service being tested:

async startScan(): Promise<string | boolean> {

    BarcodeScanner.hideBackground(); // make background of WebView transparent

    try {
      const result = await BarcodeScanner.startScan(); 
      if (result.hasContent) {
        console.log(result.content); 
        return result.content;
      }
      return false;
    } catch (error) {
      console.log("could not activate scanner. Are you on a mobile device?", error);
      return false; 
    }
  };

My mock BarcodeScanner service:

export const BarcodeScanner = {
    startScan: (options?: ScanOptions): Promise<ScanResult> => Promise.resolve({
        hasContent: true,
        content: 'mockContent',
        format: 'mockFormat'
    }),
}

My spec file:

import { ScannerService } from './scanner.service';
import { BarcodeScanner } from '../__mocks__/@capacitor/barcode-scanner';


describe('ScannerService', () => {
  let scannerService: ScannerService
  let barcodeScannerSpy: any
  let capacitorCoreSpy: any;

  beforeEach(() => {
    barcodeScannerSpy = jasmine.createSpyObj('BarcodeScanner', ['hideBackground',
      'showBackground',
      'stopScan',
      'checkPermissions',
      'openAppSettings',
      'checkPermission']);

    capacitorCoreSpy = jasmine.createSpyObj('Capacitor', ['isNativePlatform']);

    TestBed.configureTestingModule({
      providers: [
        ScannerService,
        { provide: BarcodeScanner, useValue: barcodeScannerSpy },
        { provide: Capacitor, useValue: capacitorCoreSpy }

      ]
    });

    scannerService = TestBed.inject(ScannerService) as jasmine.SpyObj<ScannerService>;
    capacitorCoreSpy.isNativePlatform.and.returnValue(true);
  });

  it('should start scan', fakeAsync(() => {
    let result: any;

    scannerService.startScan().then(res => {
      console.log("res", res);
      result = res
    });

    expect(result).toEqual('mockContent');
  }));
});

Result:

ScannerService > should start scan
Expected undefined to equal 'mockContent'.

This returns false, because it hits the catch error case, due to calling the original, which does not work in the web browser.

How do I make this call the fake spy? Thanks, help appreciated.

2

There are 2 answers

0
AliF50 On BEST ANSWER

Is BarCodeScanner an injected service? Meaning inside of the ScannerService, is it like this:

constructor(private BarCodeScanner: BarCodeScanner) {}

or

private BarCodeScanner = inject(BarCodeScanner);

I ask because BarCodeScanner is in PascalCase and usually injected services are in camelCase.

If it's an injected service, what you have is fine and maybe you need to return a value:

it('should start scan', fakeAsync(() => {
    // make the spy of startScan return a value
    barcodeScannerSpy.startScan.and.resolveTo({
        hasContent: true,
        content: 'mockContent',
        format: 'mockFormat'
    });
    let result: any;

    scannerService.startScan().then(res => {
      console.log("res", res);
      result = res
    });

    // You most likely will need a tick here to make sure
    // the promise above (.then) completed before asserting result
    tick();

    expect(result).toEqual('mockContent');
  }));

If BarCodeScanner is not an injected service (it is just an import), then unfortunately this is a limitation of TypeScript and Jasmine and you will have to mock it a different way.

You are going to have to create a Wrapper class for BarCodeScanner.

export class BarcodeScannerWrapper {
  startScan() {
    return BarcodeScanner.startScan();
  }

  hideBackground() {
    return BarcodeScanner.hideBackground();
  }
  // wrapper methods continue
}

In your service, you will have to do:

import { BarcodeScannerWrapper } from './bar-code-scanner-wrapper'; // the above file

// every time you have to use it, use the wrapper.


BarcodeScannerWrapper.hideBackground();

...

BarCodeScannerWrapper.startScan();

Then in your test, since it's not a dependency injection anymore, you will have to do the following:

// Remove this line since we are not mocking a dependency injection
// { provide: Capacitor, useValue: capacitorCoreSpy }
it('should start scan', fakeAsync(() => {
    let result: any;
    // Spy on the wrapper
    spyOn(BarcodeScannerWrapper, 'hideBackground');
    spyOn(BarcodeScannerWrapper, 'startScan').and.resolveTo({
        hasContent: true,
        content: 'mockContent',
        format: 'mockFormat'
    });

    scannerService.startScan().then(res => {
      console.log("res", res);
      result = res
    });

    tick();

    expect(result).toEqual('mockContent');
  }));

The above has been taken from here: https://stackoverflow.com/a/62935131/7365461

If none of the above works, if you can output the error, maybe that can be helpful too.

1
adamwright000 On

AliF50's answer is correct.

I should add that an eloquent way of using this wrapper in a test environment and not in a dev/prod environment without adding conditional statements to the code is to add paths to your tsconfig.spec.ts file. I implemented your solution with this:

    "paths": {
      ...,
      ...,
      "@capacitor/*": ["__mocks__/@capacitor/*"],
      ...,
      ...,

    },

This will replace every instance of that property name with the property value whenever your code is run from a spec file.