85
loading...
This website collects cookies to deliver better user experience
Unit Tests
as possible because they are cheaper (to do and to maintain).src/recipe.service.ts
export interface Recipe {
name: string;
ingredients: string[];
cookTemperature: number;
temperatureUnit: string;
steps: string;
}
export class RecipeService {
getRecipes() {
// In a real world, this is calling some backend
// through an API call
return [
{
name: "Pizza",
ingredients: ["Tomato", "Mozarella", "Basil"],
cookTemperature: 500,
temperatureUnit: 'F',
steps: "Put in oven until it gets your desired doneness"
}
];
}
}
getRecipes
that returns a list of well, recipes. In a real world scenario this would be a real HTTP call. We don't need that here.src/temperature.service.ts
export class TemperatureService {
fahrenheitToCelsius(temperature: number): number {
return ((temperature - 32) * 5) / 9;
}
}
src/recipe.component.ts
import { Recipe, RecipeService } from "./recipe.service";
import { TemperatureService } from "./temperature.service";
export class RecipeComponent {
recipes: Recipe[];
constructor(
private recipeService: RecipeService,
private temperatureService: TemperatureService
) {}
fetchRecipes() {
this.recipes = this.recipeService.getRecipes();
}
printRecipesInCelsius() {
return this.recipes.map((recipe) => {
const cookTemperature = this.temperatureService.fahrenheitToCelsius(
recipe.cookTemperature
);
return {
...recipe,
temperatureUnit: 'C',
cookTemperature
};
});
}
}
src/recipe.component.spec.ts
import { RecipeComponent } from "./recipe.component";
describe("RecipeComponent", () => {
let component: RecipeComponent;
beforeEach(() => {
component = /* what goes here? */
});
});
recipeService.getRecipes()
returns a list of recipes. We have to assume that the service itself is tested. The component boundaries ends on "I call this method in the server that is supposed to return me recipes".RecipeService
into our component
we are coupling our tests with a real service. If that service calls a slow third party backend to fetch recipes, our tests won't be fast nor reliable.RecipeService
here because it will only add complexity to our test, and as I said at the beginning, in a unit test, we need to test our piece of code in isolation.src/recipe.component.spec.ts
import { RecipeComponent } from "./recipe.component";
import { RecipeService } from "./recipe.service";
const recipeServiceMock: RecipeService = {
getRecipes: () => []
}
describe("RecipeComponent", () => {
let component: RecipeComponent;
beforeEach(() => {
// ommited for now
});
});
recipeServiceMock
is a mock of RecipeService
. It has the same interface (the getRecipes
method). It just returns an empty array. And that is perfectly fine. We just need to know that its methods are used by our SUT (subject under test, AKA the piece of code we are testing).src/recipe.component.spec.ts
describe("RecipeComponent", () => {
let component: RecipeComponent;
beforeEach(() => {
component = new RecipeComponent(recipeServiceMock, ...)
});
});
TemperatureService
.src/recipe.component.spec.ts
import { RecipeComponent } from "./recipe.component";
import { RecipeService } from "./recipe.service";
import { TemperatureService } from "./temperature.service";
const recipeServiceMock: RecipeService = {
getRecipes: () => []
}
const temperatureServiceMock: TemperatureService = {
fahrenheitToCelsius: () => 0
}
describe("RecipeComponent", () => {
let component: RecipeComponent;
beforeEach(() => {
component = new RecipeComponent(recipeServiceMock, temperatureServiceMock);
});
});
src/recipe.component.spec.ts
it("calls a service to fetch the recipes", () => {
component.fetchRecipes();
});
fetchRecipes
method, that yes, it is supposed to call the service. But we aren't sure. How can we assert this?src/recipe.component.spec.ts
import { RecipeComponent } from "./recipe.component";
import { RecipeService } from "./recipe.service";
import { TemperatureService } from "./temperature.service";
const recipeServiceMock: RecipeService = {
getRecipes: jest.fn()
}
const temperatureServiceMock: TemperatureService = {
fahrenheitToCelsius: jest.fn()
}
getRecipes
and fahrenheitToCelsius
are empty functions like before, but decorated with spying technology.src/recipe.component.spec.ts
it("calls a service to fetch the recipes", () => {
component.fetchRecipes();
expect(recipeServiceMock.getRecipes).toHaveBeenCalled();
});
fetchRecipes
and we expect getRecipes
from our RecipeService
to have been called.src/recipe.component.spec.ts
import { RecipeComponent } from "./recipe.component";
import { Recipe, RecipeService } from "./recipe.service";
import { TemperatureService } from "./temperature.service";
const recipes: Recipe[] = [
{
name: "Chicken with cream",
ingredients: ["chicken", "whipping cream", "olives"],
cookTemperature: 400,
temperatureUnit: 'F',
steps: "Cook the chicken and put in the oven for 25 minutes"
}
];
const recipeServiceMock: RecipeService = {
getRecipes: jest.fn().mockReturnValue(recipes)
};
.mockReturnValue
to our spy so it also returns a value.src/recipe.component.spec.ts
it("calls a service to fetch the recipes", () => {
component.fetchRecipes();
expect(component.recipes).toBe(recipes);
expect(recipeServiceMock.getRecipes).toHaveBeenCalled();
});
NOTE: We can have as many expectations as we need in a single test. It is not limited to just one.
src/recipe.component.spec.ts
it('can print the recipes with celsius using a service', () => {
component.fetchRecipes();
expect(component.recipes[0].cookTemperature).toBe(400);
expect(component.recipes[0].temperatureUnit).toBe('F');
const recipesInCelsius = component.printRecipesInCelsius();
const recipe = recipesInCelsius.pop();
expect(recipe.cookTemperature).not.toBe(400);
expect(recipe.temperatureUnit).toBe('C');
expect(temperatureServiceMock.fahrenheitToCelsius).toHaveBeenCalledWith(400);
});
fetchRecipes
to populate the component's recipes. Then before we do any change, we assert that the current temperature and unit are the default ones.printRecipesInCelsius
and we assert that the cookTemperature
is no longer 400 (we don't care about the exact number in this test. We assume that is tested in the service's tests) and also that the unit is 'C'.RecipeService
would use HTTP calls to retrieve the recipes, but the TemperatureService
is that simple that it won't affect our tests at all.src/recipe.component.spec.ts
const recipeServiceMock: RecipeService = {
getRecipes: jest.fn().mockReturnValue(recipes)
};
const temperatureService = new TemperatureService();
describe("RecipeComponent", () => {
let component: RecipeComponent;
beforeEach(() => {
component = new RecipeComponent(recipeServiceMock, temperatureService);
});
TemperatureService
. For this to work, we need to comment out a line of our test.src/recipe.component.spec.ts
it('can print the recipes with celsius using a service', () => {
component.fetchRecipes();
expect(component.recipes[0].cookTemperature).toBe(400);
expect(component.recipes[0].temperatureUnit).toBe('F');
const recipesInCelsius = component.printRecipesInCelsius();
const recipe = recipesInCelsius.pop();
expect(recipe.cookTemperature).not.toBe(400);
expect(recipe.temperatureUnit).toBe('C');
// expect(temperatureServiceMock.fahrenheitToCelsius).toHaveBeenCalledWith(400);
});
src/recipe.component.spec.ts
it('can print the recipes with celsius using a service', () => {
jest.spyOn(temperatureService, 'fahrenheitToCelsius');
component.fetchRecipes();
expect(component.recipes[0].cookTemperature).toBe(400);
expect(component.recipes[0].temperatureUnit).toBe('F');
const recipesInCelsius = component.printRecipesInCelsius();
const recipe = recipesInCelsius.pop();
expect(recipe.cookTemperature).not.toBe(400);
expect(recipe.temperatureUnit).toBe('C');
expect(temperatureService.fahrenheitToCelsius).toHaveBeenCalledWith(400);
});
jest.spyOn
is the same as using jest.fn
before but applied to an existing method. In this case it will also call the real service, but as we said before, it is small and simple so it doesn't really matter.