Add tests of task services

This commit is contained in:
Rayan Konecny do Nascimento 2025-04-15 22:04:56 +00:00
parent b7f4c3687e
commit 7795e5d674
11 changed files with 4074 additions and 79 deletions

View file

@ -77,24 +77,9 @@
"builder": "@angular-devkit/build-angular:extract-i18n" "builder": "@angular-devkit/build-angular:extract-i18n"
}, },
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-builders/jest:run",
"options": { "options": {
"polyfills": [ "configPath": "./jest.config.js"
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.css"
],
"scripts": []
} }
} }
} }

6
jest.config.js Normal file
View file

@ -0,0 +1,6 @@
const presets = require("jest-preset-angular/presets");
module.exports = {
...presets.createCjsPreset(),
setupFilesAfterEnv: ["<rootDir>/setup-jest.ts"],
};

3718
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@
"build": "ng build", "build": "ng build",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test", "test": "ng test",
"test:watch": "ng test --watch",
"serve:ssr:my-task-board-angular": "node dist/my-task-board-angular/server/server.mjs" "serve:ssr:my-task-board-angular": "node dist/my-task-board-angular/server/server.mjs"
}, },
"private": true, "private": true,
@ -29,20 +30,18 @@
"postcss": "^8.5.3", "postcss": "^8.5.3",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",
"tslib": "^2.3.0", "tslib": "^2.3.0"
"zone.js": "~0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/jest": "^19.0.1",
"@angular-devkit/build-angular": "^19.2.7", "@angular-devkit/build-angular": "^19.2.7",
"@angular/cli": "^19.2.7", "@angular/cli": "^19.2.7",
"@angular/compiler-cli": "^19.2.0", "@angular/compiler-cli": "^19.2.0",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.14",
"@types/node": "^18.18.0", "@types/node": "^18.18.0",
"typescript": "~5.7.2" "jest": "^29.7.0",
"typescript": "~5.7.2",
"zone.js": "^0.15.0"
} }
} }
// npm uninstall @types/jasmine jasmine-core karma, karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter
// npm install --save-dev jest @types/jest @angular-builders/jest @angular-builders/jest-preset-angular jest-preset-angular ts-jest

3
setup-jest.ts Normal file
View file

@ -0,0 +1,3 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// setupZoneTestEnv({});

48
src/app/__mocks__/task.ts Normal file
View file

@ -0,0 +1,48 @@
import { Task } from '../features/task/model/task.model';
export const tasks: Task[] = [
{
id: '1',
title: 'Ir na academia',
isCompleted: false,
categoryId: '5',
},
{
id: '2',
title: 'Comprar pão na padaria',
isCompleted: true,
categoryId: '1',
},
];
export const task: Task = {
id: '1',
title: 'Ir na academia',
isCompleted: false,
categoryId: '5',
};
export const TASK_INTERNAL_SERVER_ERROR_RESPONSE: {
status: number;
statusText: string;
} = {
status: 500,
statusText: 'Internal Server Error',
}
export const TASK_UNPROCESSIBLE_ENTITY_RESPONSE: {
status: number;
statusText: string;
} = {
status: 422,
statusText: 'Unprocessable Entity',
};
export const TASK_NOT_FOUND_RESPONSE: {
status: number;
statusText: string;
} = {
status: 404,
statusText: 'Not found',
};

View file

@ -0,0 +1,18 @@
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { provideHttpClient } from '@angular/common/http';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
providers: [provideHttpClient()],
}).compileComponents();
});
it ('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});

View file

@ -0,0 +1,270 @@
import { task, TASK_INTERNAL_SERVER_ERROR_RESPONSE, TASK_UNPROCESSIBLE_ENTITY_RESPONSE, tasks } from './../../../__mocks__/task';
import { TestBed, waitForAsync } from '@angular/core/testing';
import { HttpErrorResponse, provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TaskService } from './task.service';
describe('TaskService', () => {
let taskService: TaskService;
let httpTestingController: HttpTestingController;
const MOCKED_TASKS = tasks;
const MOCKED_TASK = task;
const baseURL = 'http://localhost:3000';
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()],
});
taskService = TestBed.inject(TaskService);
httpTestingController = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpTestingController.verify();
});
it('should be created ', () => {
expect(taskService).toBeTruthy();
});
it('should get tasks', () => {
const sortedTasks = taskService.getSortedTasks(MOCKED_TASKS);
expect(sortedTasks[0].title).toEqual('Comprar pão na padaria');
});
describe('getTasks', () => {
it('should return a list of tasks', waitForAsync(() => {
taskService.getTasks().subscribe((response) => {
expect(response).toEqual(MOCKED_TASKS);
expect(taskService.tasks()).toEqual(MOCKED_TASKS);
});
const req = httpTestingController.expectOne(`${baseURL}/tasks`);
req.flush(MOCKED_TASKS);
expect(taskService.tasks()).toEqual(MOCKED_TASKS);
expect(req.request.method).toEqual('GET');
}));
it('should throw and error when server return Internal Server Error', waitForAsync(() => {
let httpErrorResponse: HttpErrorResponse | undefined;
taskService.getTasks().subscribe({
next: () => {
fail('failed to fetch the tasks list');
},
error: (error: HttpErrorResponse) => {
httpErrorResponse = error;
},
});
const req = httpTestingController.expectOne(`${baseURL}/tasks`);
req.flush('Internal Server Error', TASK_INTERNAL_SERVER_ERROR_RESPONSE);
if (!httpErrorResponse) {
throw new Error('Error neets to be defined');
}
expect(httpErrorResponse.status).toEqual(500);
expect(httpErrorResponse.statusText).toEqual('Internal Server Error');
}));
});
describe('creatTask', () => {
it('should create a new task', waitForAsync(() => {
taskService.createTask(MOCKED_TASK).subscribe(() => {
expect(taskService.tasks()[0]).toEqual(MOCKED_TASK);
expect(taskService.tasks().length).toEqual(1);
});
const req = httpTestingController.expectOne(`${baseURL}/tasks`);
req.flush(MOCKED_TASK);
expect(req.request.method).toEqual('POST');
}));
it('should throw unprocessible entity with invalid body when create a task', waitForAsync(() => {
let httpErrorResponse: HttpErrorResponse | undefined;
taskService.createTask(MOCKED_TASK).subscribe({
next: () => {
fail('failed to fetch a new task');
},
error: (error: HttpErrorResponse) => {
httpErrorResponse = error;
},
});
const req = httpTestingController.expectOne(`${baseURL}/tasks`);
req.flush('Unprocessable Entity', TASK_UNPROCESSIBLE_ENTITY_RESPONSE);
if (!httpErrorResponse) {
throw new Error('Error neets to be defined');
}
expect(httpErrorResponse.status).toEqual(422);
expect(httpErrorResponse.statusText).toEqual('Unprocessable Entity');
}));
});
describe('updateTask', () => {
it('should update a task', waitForAsync(() => {
taskService.tasks.set([MOCKED_TASK]);
const updatedTask = MOCKED_TASK;
updatedTask.title = 'Ir na academia treinar perna';
taskService.updateTask(updatedTask).subscribe(() => {
expect(taskService.tasks()[0].title).toEqual(
'Ir na academia treinar perna'
);
});
const req = httpTestingController.expectOne(
`${baseURL}/tasks/${updatedTask.id}`
);
req.flush(MOCKED_TASK);
expect(req.request.method).toEqual('PUT');
}));
it('should throw unprocessible entity with invalid body when update a task', waitForAsync(() => {
let httpErrorResponse: HttpErrorResponse | undefined;
taskService.tasks.set([MOCKED_TASK]);
const updatedTask = MOCKED_TASK;
updatedTask.title = 'Ir na academia treinar perna';
taskService.updateTask(MOCKED_TASK).subscribe({
next: () => {
fail('failed to update a task');
},
error: (error: HttpErrorResponse) => {
httpErrorResponse = error;
},
});
const req = httpTestingController.expectOne(
`${baseURL}/tasks/${updatedTask.id}`
);
req.flush('Unprocessable Entity', TASK_UNPROCESSIBLE_ENTITY_RESPONSE);
if (!httpErrorResponse) {
throw new Error('Error neets to be defined');
}
expect(httpErrorResponse.status).toEqual(422);
expect(httpErrorResponse.statusText).toEqual('Unprocessable Entity');
}));
});
describe('updateIsCompletedStatus', () => {
it('should update IsCompletedStatus of a task', waitForAsync(() => {
const updatedTask = MOCKED_TASK;
const methodUrl = `${baseURL}/tasks/${updatedTask.id}`;
taskService.tasks.set(MOCKED_TASKS);
taskService
.updateIsCompletedStatus(MOCKED_TASK.id, true)
.subscribe(() => {
expect(taskService.tasks()[0].isCompleted).toBeTruthy();
});
const req = httpTestingController.expectOne(methodUrl);
req.flush({ isCompleted: true });
expect(req.request.method).toEqual('PATCH');
}));
it('should throw and error when update a tasks isCompleted status', waitForAsync(() => {
let httpErrorResponse: HttpErrorResponse | undefined;
const updatedTask = MOCKED_TASK;
const methodUrl = `${baseURL}/tasks/${updatedTask.id}`;
taskService.tasks.set(MOCKED_TASKS);
taskService.updateIsCompletedStatus(updatedTask.id, true).subscribe({
next: () => {
fail('failed to update a task isCompleted status');
},
error: (error: HttpErrorResponse) => {
httpErrorResponse = error;
},
});
const req = httpTestingController.expectOne(methodUrl);
req.flush('Unprocessable Entity', TASK_UNPROCESSIBLE_ENTITY_RESPONSE);
if (!httpErrorResponse) {
throw new Error('Error neets to be defined');
}
expect(httpErrorResponse.status).toEqual(422);
expect(httpErrorResponse.statusText).toEqual('Unprocessable Entity');
}));
});
describe('deleteTask', () => {
it('should delete a task', waitForAsync(() => {
taskService.tasks.set([MOCKED_TASK]);
taskService.deleteTask(MOCKED_TASK.id).subscribe(() => {
expect(taskService.tasks().length).toEqual(0);
});
const req = httpTestingController.expectOne(
`${baseURL}/tasks/${MOCKED_TASK.id}`
);
req.flush(null);
expect(req.request.method).toEqual('DELETE');
}));
it('should throw unprocessible entity with invalid body when delete a task', waitForAsync(() => {
let httpErrorResponse: HttpErrorResponse | undefined;
taskService.tasks.set([MOCKED_TASK]);
taskService.deleteTask(MOCKED_TASK.id).subscribe({
next: () => {
fail('failed to delete a task');
},
error: (error: HttpErrorResponse) => {
httpErrorResponse = error;
},
});
const req = httpTestingController.expectOne(
`${baseURL}/tasks/${MOCKED_TASK.id}`
);
req.flush('Unprocessable Entity', TASK_UNPROCESSIBLE_ENTITY_RESPONSE);
if (!httpErrorResponse) {
throw new Error('Error neets to be defined');
}
expect(httpErrorResponse.status).toEqual(422);
expect(httpErrorResponse.statusText).toEqual('Unprocessable Entity');
}));
});
});

View file

@ -19,19 +19,27 @@ export class TaskService {
public getTasks(): Observable<Task[]> { public getTasks(): Observable<Task[]> {
return this.httpClient.get<Task[]>(`${this._apiUrl}/tasks`).pipe( return this.httpClient.get<Task[]>(`${this._apiUrl}/tasks`).pipe(
tap((tasks) => { tap((tasks) => {
// Sort the tasks by title
const sortedTasks = this.getSortedTasks(tasks); const sortedTasks = this.getSortedTasks(tasks);
this.tasks.set(sortedTasks); this.tasks.set(sortedTasks);
}) })
); );
} }
public createTask(task: Partial<Task>): Observable<Task> { public getSortedTasks(tasks: Task[]): Task[] {
return this.httpClient.post<Task>(`${this._apiUrl}/tasks`, task); return tasks.sort((a, b) => a.title?.localeCompare(b.title));
} }
public getSortedTasks(tasks: Task[]): Task[] { public createTask(task: Partial<Task>): Observable<Task> {
return tasks.sort((a, b) => a.title.localeCompare(b.title)); return this.httpClient
.post<Task>(`${this._apiUrl}/tasks`, task)
.pipe(tap((tasks) => this.insertATaskInTheTasksList(tasks)));
}
public insertATaskInTheTasksList(newTask: Task): void {
const updatedTasks = [...this.tasks(), newTask];
const sortedTasks = this.getSortedTasks(updatedTasks);
this.tasks.set(sortedTasks);
} }
public insertATasksList(newTask: Task): void { public insertATasksList(newTask: Task): void {
@ -41,32 +49,32 @@ export class TaskService {
} }
public updateTask(updatedTask: Task): Observable<Task> { public updateTask(updatedTask: Task): Observable<Task> {
return this.httpClient.put<Task>( return this.httpClient
`${this._apiUrl}/tasks/${updatedTask.id}`, .put<Task>(`${this._apiUrl}/tasks/${updatedTask.id}`, updatedTask)
updatedTask .pipe(tap((tasks) => this.updateTaskInTheTasksList(tasks)));
);
} }
public updateIsCompletedStatus(taskId: string, isCompleted: boolean): Observable<Task> { public updateIsCompletedStatus(taskId: string, isCompleted: boolean ): Observable<Task> {
return this.httpClient.patch<Task>( return this.httpClient
`${this._apiUrl}/tasks/${taskId}`, .patch<Task>(`${this._apiUrl}/tasks/${taskId}`, { isCompleted })
{ isCompleted } .pipe(tap((tasks) => this.updateTaskInTheTasksList(tasks)));
);
} }
public updateTaskInTheTasksList(updatedTask: Task): void { public updateTaskInTheTasksList(updatedTask: Task): void {
this.tasks.update((tasks) => { this.tasks.update((tasks) => {
const allTasksWithUpdatedTaskRemoved = tasks.filter( const allTasksWithUpdatedTaskRemoved = tasks.filter(
(task) => task.id !== updatedTask.id (task) => task.id !== updatedTask.id
); );
const updatedTaskList = [...allTasksWithUpdatedTaskRemoved, updatedTask]; const updatedTaskList = [...allTasksWithUpdatedTaskRemoved, updatedTask];
return this.getSortedTasks(updatedTaskList); return this.getSortedTasks(updatedTaskList);
});} });
}
public deleteTask(taskId: string): Observable<Task> { public deleteTask(taskId: string): Observable<Task> {
return this.httpClient.delete<Task>(`${this._apiUrl}/tasks/${taskId}`); return this.httpClient
.delete<Task>(`${this._apiUrl}/tasks/${taskId}`)
.pipe(tap(() => this.deleteATaksInTheTasksList(taskId)));
} }
public deleteATaksInTheTasksList(taskId: string): void { public deleteATaksInTheTasksList(taskId: string): void {

View file

@ -3,6 +3,10 @@
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"types": [
"node",
"jest"
],
"outDir": "./dist/out-tsc", "outDir": "./dist/out-tsc",
"strict": true, "strict": true,
"noImplicitOverride": true, "noImplicitOverride": true,

View file

@ -5,7 +5,7 @@
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/spec", "outDir": "./out-tsc/spec",
"types": [ "types": [
"jasmine" "jest"
] ]
}, },
"include": [ "include": [