Injecting a new instance to test Angular (singleton) service persistence behavior

Viewed 638

I have an Angular Service that uses another Service to persist some of its data between page loads:

@Service({providedIn: "root"})
class PersistenceService {
  save(key: string, value: string) { ... }
  load(key: string): string { ... }
}

@Service({providedIn: "root"})
class DataService {
  user?: string;
  constructor(private persistence: PersistenceService) {}

  init() { this.user = this.persistence.load("user"); }

  setUser(str: string) {
    this.user = str;
    this.persistence.save("user", str);
  }
}

When I want to test DataService, I inject a stubbed-out Spy for PersistenceService using the Angular TestBed class:

beforeEach(() => {
  fakeValues = {};
  persistence = jasmine.createSpyObj("PersistenceService", ["save", "load"]);
  persistence.load.and.callFake(key => fakeValues[key]);
  TestBed.configureTestingModule({
    providers: [
      DataService,
      {provide: PersistenceService, useValue: persistence},
    ]
  });
  instance = TestBed.inject(DataService);
})

I want to test the way that DataService uses persisted values. I can do this easily enough by manipulating the mock PersistenceService (fakeValues in the above example). For example:

it("saves the user", () => {
  instance.init();
  instance.setUser("Joe");
  expect(persistence.save).toHaveBeenCalledWith("user, "Joe");
});

it("loads the user", () => {
  fakeValues.user = "Joe";
  instance.init();
  expect(instance.user).toBe("Joe");
});

I think this might be the wrong way to do this, though, because it requires that I know an implementation detail of DataService: the fact that the user is persisted using the key "user". What I actually want to test is, when I call setUser("Joe"), then reload the page (making a new instance of the DataService), check that instance.user is pulled from the persistence service, and gets set to "Joe".

In the above example, I could of course just call the init method again, or construct a second instance directly with new DataService(TestBed.inject(PersistenceService)), but in my real code, it might be impossible to reset the state of the service instance and re-initialize, and some Services have 4-6 dependencies so constructing one manually is a hassle. It seems like a better pattern would be one where I can inject the service-under-test, make some changes, then simulate a page reload and create a whole new instance, and check its behavior, all in a single test case. Is there an easy way to do that?

1 Answers

I was overthinking this.

Calling TestBed.resetTestingModule() destroys all singleton Service instances. Thus, I can move the setup behavior from beforeEach into its own function (let's call it setup), then in a test, I can just do:

instance.init();
expect(instance.user).toBeUndefined();
instance.setUser("Joe");

// Simulate page reload
TestBed.resetTestingModule();
setup();

expect(instance.user).toBeUndefined();
instance.init();
expect(instance.user).toBe("Joe");
Related