0

I would like to understand how to test the service with dependencies created in Angular. This service has just one method which opening a Angular Material Snackbar and doesn't return anything. Depends on conditions there are two options to display - success or error. The service doesn't have public properties, everything is done in the only method it contains. The service has also some dependencies. Pseudocode below:

export class SnackbarService {

  constructor(private snackBar: MatSnackBar, private status: Status) { }

  openSnackBar(arg: string | Error):void {
    let prop1: string;
    let prop2: number;
    let prop3: string;

    if (arg instanceof Error) {
      prop1 = this.status.default;
      prop2 = 5000;
      prop3 = 'error';
    } else {
      prop1 = 'another status';
      prop2 = 6000;
      prop3 = 'success';
    }

    this.snackBar.open(prop1, 'x', {
      prop2,
      horizontalPosition: 'center',
      verticalPosition: 'top',
      prop3
    });

}

Now the tests. I was thinking it would be probably easier to test if above service would be written in another way - with public properties passed as arguments into two methods instead of one - the one which resolve the if statement and another one which actually opens the Snackbar. However let's say the code looks like above. What can be actually tested when method doesn't return anything and there are no public properties?

describe('SnackbarService', () => {
  let service: SnackbarService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        MatSnackBarModule
      ],
      providers: [
        SnackbarService
      ]
    });
    service = TestBed.inject(SnackbarService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

I know that dependencies should be imported to the tests but have no idea how to test anything else than the creation of the service itself. Appreciate any constructive hints!

mpro
  • 14,302
  • 5
  • 28
  • 43

3 Answers3

1

Since MatSnackBar is being Dependency Injected, you can mock it, spy on its open method, and verify the arguments being passed to it.

So your test setup would look like this:

const mockMatSnackBar = {
  open: () => {}
};

const mockStatus = {
  default: ''
};

beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [
    ],
    providers: [
      SnackbarService,
      { provide: MatSnackBar, useValue: mockMatSnackBar }, // <--- use mock
      { provide: Status, useValue: mockStatus } // <--- use mock
    ]
  });
  service = TestBed.inject(SnackbarService);
  matSnackBar = TestBed.inject(MatSnackBar);
});

And your test would look like this, spying on the open method and verifying the arguments it gets invoked with:

it('calls the MatSnackBar open method with status, "x", and a config object', () => {
  const matSnackBarSpy = spyOn(matSnackBar, 'open').and.stub();

  service.openSnackBar('arg');

  expect(matSnackBarSpy.calls.count()).toBe(1);

  const args = matSnackBarSpy.calls.argsFor(0);
  expect(args.length).toBe(3);
  expect(args[0]).toBe('another status');
  expect(args[1]).toBe('x');
  expect(args[2]).toEqual({
    prop2: 6000,
    horizontalPosition: 'center',
    verticalPosition: 'top',
    prop3: 'success'
  });
});

Here's a StackBlitz showing that approach.

MikeJ
  • 2,179
  • 13
  • 16
  • Thanks for your input. I've already write some basic test and posted an answer, but it is actually good to see your richer I think approach and compare with mine. Could you have a look and comment it? I'll reuse your StackBliz and paste the link. – mpro Oct 20 '20 at 07:23
  • @mpro, I added a comment to your posted answer, per your request. :) I hope it's helpful. – MikeJ Oct 20 '20 at 19:40
0

You can validate that it should open.

it('should open snack bar', () => {
  // use this for fake call or return value or use next line
  spyOn(service, 'openSnackBar').and.callThrough(); 
  // it will call the service 
  service.openSnackBar(); 
  expect(service.openSnackBar('open this')).toHaveBeenCalled();
});
Liam
  • 27,717
  • 28
  • 128
  • 190
uiTeam324
  • 1,215
  • 15
  • 34
0

Based on some yesterdays hints (which some already disappeared), Jasmine docs and this post I was able to write some basic test (description in comments and working StackBlitz)

describe("SnackbarService", () => {
  let service: SnackbarService;
  // mocking service
  const mockSnackbarService = jasmine.createSpyObj("SnackbarService", [
    "openSnackBar"
  ]);

  beforeEach(() => {
    TestBed.configureTestingModule({
      // providing the service and mock into the Testbed
      providers: [{ provide: SnackbarService, useValue: mockSnackbarService }]
    });
    // injecting the service
    service = TestBed.inject(SnackbarService);
  });

  it("should be created", () => {
    expect(service).toBeTruthy();
  });

  describe("openSnackBar", () => {
    it("should open the snack bar", () => {
      const arg = "test";
      // calling mock
      mockSnackbarService.openSnackBar.and.callThrough();
      // calling service method with argument
      service.openSnackBar(arg);
      // checking if method has been called
      expect(service.openSnackBar).toHaveBeenCalled();
    });
  });
});
mpro
  • 14,302
  • 5
  • 28
  • 43
  • You shouldn't be mocking `SnackbarService` if that's the code you're testing, you should be mocking its dependencies, otherwise you're not executing (and testing) the app code. And this test isn't very useful because you're calling a method in your test, then `expect`ing that method to have been called, but you know it's been called because you just called it. A useful test would be to call a service method in your test and `expect` that something the app code should have done was actually done, like calling some dependency's method. But you're not even exercising the app code here. – MikeJ Oct 20 '20 at 19:26