How to mock environment files import in unit tests
Asked Answered
C

5

26

In our angular app, we use environment files to load some config.

environment.ts

export const environment = {
  production: false,
  defaultLocale: 'en_US',
};

We then use it in one of our service:

import { environment } from '../../environments/environment';
import { TranslateService } from './translate.service';

@Injectable()
export class LocaleService {
  constructor(private translateService: TranslateService){}

  useDefaultLocaleAsLang(): void {
    const defaultLocale = environment.defaultLocale;
    this.translateService.setUsedLang(defaultLocale);
  }
}

So I use the values in environment file in a service method.

In our test file, we can of course Spy on the translateService:

translateService = jasmine.createSpyObj('translateService', ['setUsedLang']);

But I don't know how to mock the environment values in my testing file (in a beforeEach for example). Or even transform it, for testing purpose, to a Subject so I can change it and test different behaviors.

More generally speaking, how can you mock such imports values in tests to be sure not to use real values?

Chelseychelsie answered 4/3, 2018 at 2:35 Comment(2)
Wrap the environment in an interface which loads it (EnvironmentLoader) and mock that?Aged
Yes I could but then how to test the EnvironmentLoader class and methods? The problem is moved but not fixed.Chelseychelsie
C
24

You can't test/mock environment.ts. It is not part of Angular's DI system, it is a hard dependency on a file on the filesystem. Angular's compilation process is what enables swapping out the different environment.*.ts files under the hood when you do a build.

Angular's DI system is a typical Object Oriented approach for making parts of your application more testable and configurable.

My recommendation would be to take advantage of the DI system and use something like this sparingly

import { environment } from '../../environments/environment';

Instead do the same thing Angular does for any dependencies it wants you to be abstracted away from. Make a service that provides a seam between the environment.ts data and your application pieces.

It doesn't need to have any logic, it could simply pass through the properties of environment directly (therefore it wouldn't need tested itself).

Then update your services/components that depend on environment.ts and replace that dependency with the service. At test time you can mock it, sourcing the data from somewhere other than environment.ts

Carriole answered 17/8, 2018 at 23:10 Comment(6)
You can find a ckear description how to create an abstraction using an injection token here: nils-mehlhorn.de/posts/angular-environment-setup-testingSanctuary
But you still need to test it inside the service, how would you do it ?Estellestella
@VictorZakharov are you trying to achieve 100% test coverage or are you trying to effectively test your application? These aren't the same thing. If you are trying to test the environment import, you are basically testing Angular, which is going to have a very low value to cost ratio.Carriole
Going after 100% coverage.Estellestella
@VictorZakharov then look in the Angular source code to see how they test the environment files and copy their tests into your project. You'll get 100% coverage and (in my opinion) achieved very little.Carriole
100% coverage is useful when you have slackers on your team (offshore, low performer etc). If the standard is 90%, then some members will go for 80% and some will go closer to 100%. Overall, 90% standard is satisfied, but some developers did 10x more work than others. 100% coverage ensures fairness in workload. It also ensures a lot higher code quality, because difficulty of covering the remaining 20% is proportional to the amount of tech debt the code has. Want to easily cover 100%? Have no tech debt. Which means refactor early.Estellestella
F
11

Something like this worked for me:

it('should ...', () => {
  environment.defaultLocale = <location-to-test>; // e.g. 'en'

  const result = service.method();

  expect(result).toEqual(<expected-result>);
});
Footloose answered 28/4, 2020 at 14:34 Comment(2)
I think the problem with this approach is that its chaging the global variable, so it may collide with other test that needs a different value in order to workChlorite
@AlejandroBarone good point. Maybe reseting the environment.defaultLocale back to the orginal value in the afterEach is an option.Footloose
M
9

A technique I use in situations like this is to create a wrapper service e.g. EnvironmentService.ts and in this case it returns the environment configuration.

This allows me to mock calls to the EnvironmentService's getEnvironmentConfiguration method just like any other spyOn call.

This then allows for the modification of environment variables in unit tests :)

Mayers answered 10/10, 2019 at 1:37 Comment(1)
This is really the correct answer. Wrappers around difficult to test code as an API layer is a standard practice. It also conform to SOLID principles.Plenary
B
8

With jest, you can use this to mock your angular environment:

import { environment } from 'path/to/environments/environment';
jest.mock('path/to/environments/environment', () => ({
  environment: {
    production: false,
    defaultLocale: 'en_US',
  },
}));
Brand answered 3/2, 2022 at 10:10 Comment(1)
And with jasmine? OP is asking about jasmine solution.Estellestella
T
0

Considering the above example from question

import { environment } from '../../environments/environment';
import { TranslateService } from './translate.service';

@Injectable()
export class LocaleService {
  constructor(private translateService: TranslateService){}

  useDefaultLocaleAsLang(): void {
    const defaultLocale = environment.defaultLocale;
    this.translateService.setUsedLang(defaultLocale);
  }
}

To load the environment variables in your test file, you can simply merge environment variables with required keys with lodash, as given below

In following test i am setting "Hindi" for environment variables and testing its set in service test call

import { environment } from 'path/to/environments/environment';
import * as _ from 'lodash';

_.merge(environment, {
  defaultLocale: 'Hindi'
});

describe('LocaleService Tests', () => {
  let service: LocaleService;
  let translateServiceSpy: jasmine.SpyObj<TranslateService>;

  beforeEach(async () => {
    translateSpy = jasmine.createSpyObj('TranslateService',['setUsedLang']);
    
    TestBed.configureTestingModule({
      providers: [
        LocaleService,
        { provide: TranslateService, useValue: translateSpy }
      ]
    });
    service = TestBed.inject(LocaleService);
    
    translateServiceSpy = TestBed.inject(TranslateService) as jasmine.SpyObj<TranslateService>;
  });

  it('should set the lang environment', () => {
    service.useDefaultLocaleAsLang();
    
   //assert : translate service should set Hindi as default language from above environment
   expect(translateServiceSpy.setUsedLang).toHaveBeenCalledOnceWith('Hindi');
  });
});
Token answered 14/6, 2023 at 12:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.