From dfacc39e57d823841a3bc2641d59e17064a1d2f1 Mon Sep 17 00:00:00 2001 From: Gabriel De Los Rios Date: Sun, 18 Jan 2026 18:36:34 -0300 Subject: [PATCH] test: add unit tests for DAOs --- src/app/dao/ChainDAO.spec.ts | 73 ++++++++++++++++ src/app/dao/EstablishmentDAO.spec.ts | 76 +++++++++++++++++ src/app/dao/EstablishmentDAO.ts | 6 +- src/app/dao/ProductDAO.spec.ts | 46 ++++++++++ src/app/dao/ProductEstablishmentDAO.spec.ts | 86 +++++++++++++++++++ src/app/dao/ProductEstablishmentDAO.ts | 16 +--- src/app/dao/PurchaseDAO.spec.ts | 95 +++++++++++++++++++++ src/app/dao/PurchaseDAO.ts | 21 +---- src/app/types/sqlite.type.ts | 39 ++++++++- 9 files changed, 420 insertions(+), 38 deletions(-) create mode 100644 src/app/dao/ChainDAO.spec.ts create mode 100644 src/app/dao/EstablishmentDAO.spec.ts create mode 100644 src/app/dao/ProductDAO.spec.ts create mode 100644 src/app/dao/ProductEstablishmentDAO.spec.ts create mode 100644 src/app/dao/PurchaseDAO.spec.ts diff --git a/src/app/dao/ChainDAO.spec.ts b/src/app/dao/ChainDAO.spec.ts new file mode 100644 index 0000000..9a3bd46 --- /dev/null +++ b/src/app/dao/ChainDAO.spec.ts @@ -0,0 +1,73 @@ +import { TestBed } from '@angular/core/testing'; +import { ChainDAO } from './ChainDAO'; +import { Sqlite } from '../services/sqlite'; +import { vi } from 'vitest'; +import { QueryResult } from '../types/sqlite.type'; +import { Chain } from '../models/Chain'; + +describe('ChainDAO', () => { + let service: ChainDAO; + let sqlite: Partial; + let CHAIN_MOCK: Chain; + let RESULTS_MOCK: QueryResult; + + beforeEach(() => { + CHAIN_MOCK = new Chain('mock', 'mock.png', 1); + RESULTS_MOCK = { rows: [CHAIN_MOCK] }; + + sqlite = { + executeQuery: vi.fn().mockResolvedValue(RESULTS_MOCK), + }; + + TestBed.configureTestingModule({ + providers: [{ provide: Sqlite, useValue: sqlite }], + }); + + service = TestBed.inject(ChainDAO); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should receive mapped results on findAll', async () => { + const result = await service.findAll(); + if (result.length > 0) { + expect(result[0]).toEqual(CHAIN_MOCK); + } else { + test.fails('Expected mock response to contain at least one value'); + } + }); + + it('should receive mapped results on findBy', async () => { + const result = await service.findBy({ id: CHAIN_MOCK.id, name: CHAIN_MOCK.name }); + if (result.length > 0) { + expect(result[0]).toEqual(CHAIN_MOCK); + } else { + test.fails('Expected mock response to contain at least one value'); + } + }); + + it('should call executeQuery with object fields on insert', async () => { + const expectedSql = 'INSERT INTO chain ( name, image, id ) VALUES ( ?, ?, ? );'; + const expectedParams: any[] = []; + for (let key in CHAIN_MOCK) { + expectedParams.push(CHAIN_MOCK[key]); + } + await service.insert(CHAIN_MOCK); + expect(sqlite.executeQuery).toHaveBeenCalledExactlyOnceWith(expectedSql, expectedParams); + }); + + it('should call executeQuery with object fields and where values on update', async () => { + const expectedParams: any[] = []; + CHAIN_MOCK.name = 'Updated mock'; + for (let key in CHAIN_MOCK) { + expectedParams.push(CHAIN_MOCK[key]); + } + const WHERE_MOCK: Partial = { id: 1 }; + expectedParams.push(WHERE_MOCK.id); + const expectedSql = 'UPDATE chain SET name = ?, image = ?, id = ? WHERE id = ? ;'; + await service.update(CHAIN_MOCK, WHERE_MOCK); + expect(sqlite.executeQuery).toHaveBeenCalledExactlyOnceWith(expectedSql, expectedParams); + }); +}); diff --git a/src/app/dao/EstablishmentDAO.spec.ts b/src/app/dao/EstablishmentDAO.spec.ts new file mode 100644 index 0000000..edfb788 --- /dev/null +++ b/src/app/dao/EstablishmentDAO.spec.ts @@ -0,0 +1,76 @@ +import { TestBed } from '@angular/core/testing'; +import { EstablishmentDAO } from './EstablishmentDAO'; +import { Sqlite } from '../services/sqlite'; +import { Establishment } from '../models/Establishment'; +import { Chain } from '../models/Chain'; +import { EstablishmentQueryResult } from '../types/sqlite.type'; + +describe('EstablishmentDAO', () => { + let service: EstablishmentDAO; + let sqlite: Partial; + let ESTABLISHMENT_MOCK: Establishment; + let QUERY_RESULT_MOCK: EstablishmentQueryResult; + + beforeEach(() => { + QUERY_RESULT_MOCK = { + address: 'mock street', + image: 'mock.jpg', + name: 'mock', + chain_id: 1, + id: 1, + }; + ESTABLISHMENT_MOCK = new Establishment( + new Chain(QUERY_RESULT_MOCK.name, QUERY_RESULT_MOCK.image, QUERY_RESULT_MOCK.chain_id), + QUERY_RESULT_MOCK.address, + QUERY_RESULT_MOCK.id, + ); + + sqlite = { + executeQuery: vi.fn().mockResolvedValue({ rows: [QUERY_RESULT_MOCK] }), + }; + + TestBed.configureTestingModule({ + providers: [{ provide: Sqlite, useValue: sqlite }], + }); + + service = TestBed.inject(EstablishmentDAO); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should receive mapped results on findAll', async () => { + const result = await service.findAll(); + if (result.length > 0) { + expect(result[0]).toEqual(ESTABLISHMENT_MOCK); + } else { + test.fails('Expected mock response to contain at least one value'); + } + }); + + it('should receive mapped results on findBy', async () => { + const result = await service.findBy({ id: 1 }); + if (result.length > 0) { + expect(result[0]).toEqual(ESTABLISHMENT_MOCK); + } else { + test.fails('Expected mock response to contain at least one value'); + } + }); + + it('should call executeQuery with object fields on insert', async () => { + sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] }); + const expectedSql = 'INSERT INTO establishment ( address, chain_id ) VALUES ( ?, ? );'; + const expectedParams = [ESTABLISHMENT_MOCK.address, ESTABLISHMENT_MOCK.chain.id]; + ESTABLISHMENT_MOCK.id = undefined; + await service.insert(ESTABLISHMENT_MOCK); + expect(sqlite.executeQuery).toHaveBeenCalledExactlyOnceWith(expectedSql, expectedParams); + }); + + it('should throw if toDB is called with a chain that has no id', async () => { + sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] }); + ESTABLISHMENT_MOCK.id = undefined; + ESTABLISHMENT_MOCK.chain.id = undefined; + expect(service.insert(ESTABLISHMENT_MOCK)).rejects.toThrow(); + }); +}); diff --git a/src/app/dao/EstablishmentDAO.ts b/src/app/dao/EstablishmentDAO.ts index 2ccbd10..5cbad99 100644 --- a/src/app/dao/EstablishmentDAO.ts +++ b/src/app/dao/EstablishmentDAO.ts @@ -3,6 +3,7 @@ import { Establishment } from '../models/Establishment'; import { Chain } from '../models/Chain'; import { DBEstablishment } from '../models/db/DBEstablishment'; import { ComposedDAO } from './ComposedDAO'; +import { EstablishmentQueryResult } from '../types/sqlite.type'; @Injectable({ providedIn: 'root', @@ -16,7 +17,7 @@ export class EstablishmentDAO extends ComposedDAO< super( Establishment.name, `SELECT e.id, e.address, chain_id, c.name, c.image FROM establishment e JOIN chain c ON c.id = chain_id;`, - 'e' + 'e', ); } @@ -30,6 +31,3 @@ export class EstablishmentDAO extends ComposedDAO< return new Establishment(chain, qR.address, qR.id); } } - -type EstablishmentQueryResult = Omit & - Omit & { id: number; chain_id: number }; diff --git a/src/app/dao/ProductDAO.spec.ts b/src/app/dao/ProductDAO.spec.ts new file mode 100644 index 0000000..72cfb5b --- /dev/null +++ b/src/app/dao/ProductDAO.spec.ts @@ -0,0 +1,46 @@ +import { TestBed } from '@angular/core/testing'; +import { ProductDAO } from './ProductDAO'; +import { Sqlite } from '../services/sqlite'; +import { Product } from '../models/Product'; + +describe('ProductDAO', () => { + let service: ProductDAO; + let sqlite: Partial; + + let PRODUCT_MOCK: Product; + + beforeEach(() => { + PRODUCT_MOCK = new Product('12121112', 'mock', 'mock.jpg', 1); + + sqlite = { + executeQuery: vi.fn().mockResolvedValue({ rows: [PRODUCT_MOCK] }), + }; + + TestBed.configureTestingModule({ + providers: [{ provide: Sqlite, useValue: sqlite }], + }); + + service = TestBed.inject(ProductDAO); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should receive mapped results on findAll', async () => { + const result = await service.findAll(); + if (result.length > 0) { + expect(result[0]).toEqual(PRODUCT_MOCK); + } else { + test.fails('Expected mock response to contain at least one value'); + } + }); + + it('should call executeQuery with object fields on insert', async () => { + PRODUCT_MOCK.id = undefined; + const expectedSql = 'INSERT INTO product ( barcode, name, image ) VALUES ( ?, ?, ? );'; + const expectedParams: any[] = [PRODUCT_MOCK.barcode, PRODUCT_MOCK.name, PRODUCT_MOCK.image]; + await service.insert(PRODUCT_MOCK); + expect(sqlite.executeQuery).toHaveBeenCalledExactlyOnceWith(expectedSql, expectedParams); + }); +}); diff --git a/src/app/dao/ProductEstablishmentDAO.spec.ts b/src/app/dao/ProductEstablishmentDAO.spec.ts new file mode 100644 index 0000000..c0273f7 --- /dev/null +++ b/src/app/dao/ProductEstablishmentDAO.spec.ts @@ -0,0 +1,86 @@ +import { TestBed } from '@angular/core/testing'; +import { ProductEstablishmentDAO } from './ProductEstablishmentDAO'; +import { Sqlite } from '../services/sqlite'; +import { ProductEstablishment } from '../models/ProductEstablisment'; +import { Product } from '../models/Product'; +import { Establishment } from '../models/Establishment'; +import { Chain } from '../models/Chain'; +import { ProductEstablishmentQueryResult } from '../types/sqlite.type'; + +describe('ProductEstablishmentDAO', () => { + let service: ProductEstablishmentDAO; + let sqlite: Partial; + + let PRODUCT_ESTABLISHMENT_MOCK: ProductEstablishment; + let PRODUCT_ESTABLISHMENT_QUERY_RESULT: ProductEstablishmentQueryResult; + + beforeEach(() => { + PRODUCT_ESTABLISHMENT_MOCK = new ProductEstablishment( + new Product('121212', 'mock', 'mock.jpg', 1), + new Establishment(new Chain('mock', 'mock.png', 1), 'mock street', 1), + 1, + ); + PRODUCT_ESTABLISHMENT_QUERY_RESULT = { + address: PRODUCT_ESTABLISHMENT_MOCK.establishment.address, + barcode: PRODUCT_ESTABLISHMENT_MOCK.product.barcode, + chain_id: PRODUCT_ESTABLISHMENT_MOCK.establishment.chain.id!, + chain_image: PRODUCT_ESTABLISHMENT_MOCK.establishment.chain.image, + chain_name: PRODUCT_ESTABLISHMENT_MOCK.establishment.chain.name, + establishment_id: PRODUCT_ESTABLISHMENT_MOCK.establishment.id!, + id: PRODUCT_ESTABLISHMENT_MOCK.id!, + product_id: PRODUCT_ESTABLISHMENT_MOCK.product.id!, + product_image: PRODUCT_ESTABLISHMENT_MOCK.product.image, + product_name: PRODUCT_ESTABLISHMENT_MOCK.product.name, + }; + + sqlite = { + executeQuery: vi.fn().mockResolvedValue({ rows: [PRODUCT_ESTABLISHMENT_QUERY_RESULT] }), + }; + + TestBed.configureTestingModule({ + providers: [{ provide: Sqlite, useValue: sqlite }], + }); + + service = TestBed.inject(ProductEstablishmentDAO); + }); + + it('should be created', () => { + expect(PRODUCT_ESTABLISHMENT_MOCK).toBeTruthy(); + }); + + it('should receive mapped results on findAll', async () => { + const result = await service.findAll(); + if (result.length > 0) { + expect(result[0]).toEqual(PRODUCT_ESTABLISHMENT_MOCK); + } else { + test.fails('Expected mock response to contain at least one value'); + } + }); + + it('should call executeQuery with object fields on insert', async () => { + sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] }); + const expectedSql = + 'INSERT INTO product_establishment ( product_id, establishment_id ) VALUES ( ?, ? );'; + const expectedParams: any[] = [ + PRODUCT_ESTABLISHMENT_MOCK.product.id, + PRODUCT_ESTABLISHMENT_MOCK.establishment.id, + ]; + PRODUCT_ESTABLISHMENT_MOCK.id = undefined; + await service.insert(PRODUCT_ESTABLISHMENT_MOCK); + expect(sqlite.executeQuery).toHaveBeenCalledExactlyOnceWith(expectedSql, expectedParams); + }); + + describe('toDB', () => { + it('should throw if toDB is called with a product that has no id', async () => { + sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] }); + PRODUCT_ESTABLISHMENT_MOCK.product.id = undefined; + expect(service.insert(PRODUCT_ESTABLISHMENT_MOCK)).rejects.toThrow(); + }); + + it('should throw if toDB is called with a establishmen that has no id', async () => { + sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] }); + PRODUCT_ESTABLISHMENT_MOCK.establishment.id = undefined; + expect(service.insert(PRODUCT_ESTABLISHMENT_MOCK)).rejects.toThrow(); + }); + }); +}); diff --git a/src/app/dao/ProductEstablishmentDAO.ts b/src/app/dao/ProductEstablishmentDAO.ts index 93a7e33..ab02b82 100644 --- a/src/app/dao/ProductEstablishmentDAO.ts +++ b/src/app/dao/ProductEstablishmentDAO.ts @@ -5,6 +5,7 @@ import { ProductEstablishment } from '../models/ProductEstablisment'; import { Chain } from '../models/Chain'; import { DBProductEstablishment } from '../models/db/DBProductEstablishment'; import { ComposedDAO } from './ComposedDAO'; +import { ProductEstablishmentQueryResult } from '../types/sqlite.type'; @Injectable({ providedIn: 'root', @@ -23,7 +24,7 @@ export class ProductEstablishmentDAO extends ComposedDAO< JOIN product p ON p.id = pe.product_id JOIN establishment e ON e.id = pe.establishment_id JOIN chain c ON c.id = e.chain_id;`, - 'pe' + 'pe', ); } @@ -40,16 +41,3 @@ export class ProductEstablishmentDAO extends ComposedDAO< return new ProductEstablishment(product, establishment, qR.id); } } - -type ProductEstablishmentQueryResult = { - address: string; - barcode: string; - chain_id: number; - chain_image: string | null; - chain_name: string; - establishment_id: number; - id: number; - product_id: number; - product_image: string | null; - product_name: string; -}; diff --git a/src/app/dao/PurchaseDAO.spec.ts b/src/app/dao/PurchaseDAO.spec.ts new file mode 100644 index 0000000..575ea45 --- /dev/null +++ b/src/app/dao/PurchaseDAO.spec.ts @@ -0,0 +1,95 @@ +import { TestBed } from '@angular/core/testing'; +import { PurchaseDAO } from './PurchaseDAO'; +import { Sqlite } from '../services/sqlite'; +import { Purchase } from '../models/Purchase'; +import { Establishment } from '../models/Establishment'; +import { Chain } from '../models/Chain'; +import { Product } from '../models/Product'; +import { PurchaseQueryResult } from '../types/sqlite.type'; + +describe('PurchaseDAO', () => { + let service: PurchaseDAO; + let sqlite: Partial; + + let PURCHASE_MOCK: Purchase; + let PURCHASE_QUERY_RESULT_MOCK: PurchaseQueryResult; + + beforeEach(() => { + PURCHASE_MOCK = new Purchase( + new Establishment(new Chain('mock', 'mock.jpg', 1), 'mock street', 1), + new Product('1212112', 'mock', 'mock.png', 1), + 1245.55, + 3, + Date.now(), + 1, + ); + PURCHASE_QUERY_RESULT_MOCK = { + address: PURCHASE_MOCK.establishment.address, + barcode: PURCHASE_MOCK.product.barcode, + chain_id: PURCHASE_MOCK.establishment.chain.id!, + chain_image: PURCHASE_MOCK.establishment.chain.image, + chain_name: PURCHASE_MOCK.establishment.chain.name, + date: PURCHASE_MOCK.date, + establishment_id: PURCHASE_MOCK.establishment.id!, + id: PURCHASE_MOCK.id!, + price: PURCHASE_MOCK.price, + product_id: PURCHASE_MOCK.product.id!, + product_image: PURCHASE_MOCK.product.image, + product_name: PURCHASE_MOCK.product.name, + quantity: PURCHASE_MOCK.quantity, + }; + + sqlite = { + executeQuery: vi.fn().mockResolvedValue({ rows: [PURCHASE_QUERY_RESULT_MOCK] }), + }; + + TestBed.configureTestingModule({ + providers: [{ provide: Sqlite, useValue: sqlite }], + }); + + service = TestBed.inject(PurchaseDAO); + }); + + it('should be created', () => { + expect(PURCHASE_MOCK).toBeTruthy(); + }); + + it('should receive mapped results on findAll', async () => { + const result = await service.findAll(); + if (result.length > 0) { + expect(result[0]).toEqual(PURCHASE_MOCK); + } else { + test.fails('Expected mock response to contain at least one value'); + } + }); + + it('should call executeQuery with object fields on insert', async () => { + sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] }); + const expectedSql = + 'INSERT INTO purchase ( establishment_id, product_id, date, price, quantity ) VALUES ( ?, ?, ?, ?, ? );'; + const expectedParams: any[] = [ + PURCHASE_MOCK.establishment.id, + PURCHASE_MOCK.product.id, + PURCHASE_MOCK.date, + PURCHASE_MOCK.price * 100, + PURCHASE_MOCK.quantity, + ]; + PURCHASE_MOCK.id = undefined; + await service.insert(PURCHASE_MOCK); + expect(sqlite.executeQuery).toHaveBeenCalledExactlyOnceWith(expectedSql, expectedParams); + }); + + describe('toDB', () => { + it('should throw if toDB is called with a establishmen that has no id', async () => { + sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] }); + PURCHASE_MOCK.establishment.id = undefined; + expect(service.insert(PURCHASE_MOCK)).rejects.toThrow(); + }); + + it('should throw if toDB is called with a product that has no id', async () => { + sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] }); + PURCHASE_MOCK.product.id = undefined; + expect(service.insert(PURCHASE_MOCK)).rejects.toThrow(); + }); + }); +}); diff --git a/src/app/dao/PurchaseDAO.ts b/src/app/dao/PurchaseDAO.ts index c342d9e..4f94a06 100644 --- a/src/app/dao/PurchaseDAO.ts +++ b/src/app/dao/PurchaseDAO.ts @@ -5,6 +5,7 @@ import { Establishment } from '../models/Establishment'; import { Chain } from '../models/Chain'; import { Product } from '../models/Product'; import { ComposedDAO } from './ComposedDAO'; +import { PurchaseQueryResult } from '../types/sqlite.type'; @Injectable({ providedIn: 'root', @@ -21,7 +22,7 @@ export class PurchaseDAO extends ComposedDAO = { rows: T[]}; -export type BatchOp = [string, any[]]; \ No newline at end of file +import { Chain } from '../models/Chain'; +import { Establishment } from '../models/Establishment'; + +export type QueryResult = { rows: T[] }; +export type BatchOp = [string, any[]]; + +export type PurchaseQueryResult = { + id: number; + price: number; + quantity: number; + date: number; + establishment_id: number; + product_id: number; + barcode: string; + product_image: string | null; + product_name: string; + address: string; + chain_id: number; + chain_image: string | null; + chain_name: string | null; +}; + +export type ProductEstablishmentQueryResult = { + address: string; + barcode: string; + chain_id: number; + chain_image: string | null; + chain_name: string | null; + establishment_id: number; + id: number; + product_id: number; + product_image: string | null; + product_name: string; +}; + +export type EstablishmentQueryResult = Omit & + Omit & { id: number; chain_id: number };