In our work, we sometimes use Angular. We create Angular services and components, and we test them. We used to test components and services together, but we found that was too complicated. Now, for component testing, we use test doubles (aka spy aka mock) to fake out the behavior of our services.

At first, we defined our test doubles like so:


describe('SockDrawerComponent', () => {
  let component: SockDrawerComponent;
  let fixture: ComponentFixture<SockDrawerComponent>;
  let sockOrganizerService: jasmine.SpyObj<SockOrganizerService>;

  beforeEach(async () => {
    sockOrganizerService = jasmine.createSpyObj('SockOrganizerService', ['sortByColor', 'eliminateRoommateSocks', 'findSingles']);
    await TestBed.configureTestingModule({
      declarations: [SockDrawerComponent],
      providers: [
        { provide: SockOrganizerService, useValue: sockOrganizerService }
      ]
    })
      .compileComponents();
  });

  // ...
});

This worked, but we found that typing out the jasmine.createSpyObj was tedious. We also ran into trouble when we would rename methods.

We use our IDE to rename methods, and it renames the method throughout our codebase, which makes renaming a low-risk activity. However, the string method names in jasmine.createSpyObj don’t get renamed, and our tests break.

For example, if we used our IDE to rename findSingles to findSingleSocks, the code is updated but the jasmine.createSpyObj is not. Our test fails because the spy has no method defined for the new name findSingleSocks.

In this case, we are renaming findSingles while we work on SockOrganizerService but the failing test is for SockDrawerComponent. We now have to go read and fix an unrelated test before we can get back to working on the service.

We want renames to be easy and painless so we have no incentive to keep an unclear name. This testing pattern was making it harder to rename, and we didn’t like that.

Enter photocopy

Our solution was to write a test helper which creates a spy object based on a reference class. Because it generates the spy based on the real code, it picks up renames automatically with no edits to the test setup. Here is how our test looks using the photocopier:


describe('SockDrawerComponent', () => {
  let component: SockDrawerComponent;
  let fixture: ComponentFixture<SockDrawerComponent>;
  let sockOrganizerService: jasmine.SpyObj<SockOrganizerService>;

  beforeEach(async () => {
    sockOrganizerService = photocopy(SockOrganizerService); // 👈 here
    await TestBed.configureTestingModule({
      declarations: [SockDrawerComponent],
      providers: [
        { provide: SockOrganizerService, useValue: sockOrganizerService }
      ]
    })
      .compileComponents();
  });

  // ...
});

We’re loving how terse it is to define new dependencies, and we appreciate the feeling of safety when renaming – we don’t worry about missing a rename in the test.

How we did it

Here is the source for the photocopy method.

/**
 * Creates a jasmine.SpyObj from the provided class. Every method (including private methods) is mocked, but no mock
 * behavior is defined for them. This is equivalent to calling jasmine.createSpyObj('YourClassName', ['every', 'method', 'on', 'your', 'class']
 * @param classReference the class to copy
 */
export function photocopy<ThingToMock extends {
  new(...x: any[]): any
}>(classReference: ThingToMock): jasmine.SpyObj<InstanceType<ThingToMock>> {
  const methods = Object.getOwnPropertyNames(classReference.prototype)
    .filter(x => x !== 'constructor');
  const name = classReference.name;
  if (!methods.length) {
    throw new Error(`Paper jam! Cannot photocopy ${name} because it has no methods defined.`);
  }
  return jasmine.createSpyObj(name, methods);
}