mirror of
https://github.com/greedychipmunk/agent-skills.git
synced 2026-09-14 20:46:42 +08:00
19 KiB
19 KiB
MedusaJS Testing Patterns
Testing Setup
Test Configuration
// jest.config.js
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
setupFilesAfterEnv: ["<rootDir>/test/setup.ts"],
testMatch: [
"**/__tests__/**/*.test.ts",
"**/test/**/*.test.ts"
],
moduleNameMapping: {
"^@/(.*)$": "<rootDir>/src/$1",
"^@test/(.*)$": "<rootDir>/test/$1"
},
collectCoverageFrom: [
"src/**/*.ts",
"!src/**/*.d.ts",
"!src/**/index.ts"
],
coverageDirectory: "coverage",
coverageReporters: ["text", "html", "lcov"]
}
Test Setup File
// test/setup.ts
import { MockContainer } from "@test/mocks/container"
import { MockDatabase } from "@test/mocks/database"
// Global test setup
beforeAll(async () => {
// Setup test database
await MockDatabase.setup()
})
afterAll(async () => {
// Cleanup test database
await MockDatabase.cleanup()
})
beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks()
})
// Mock external services
jest.mock("stripe", () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
paymentIntents: {
create: jest.fn(),
retrieve: jest.fn(),
confirm: jest.fn()
},
webhooks: {
constructEvent: jest.fn()
}
}))
}))
// Mock container resolution
jest.mock("@medusajs/framework/utils", () => ({
...jest.requireActual("@medusajs/framework/utils"),
MedusaService: jest.fn().mockImplementation(() => MockContainer)
}))
Unit Testing
Service Testing
// __tests__/services/product.test.ts
import ProductService from "@/modules/product/services/product"
import { MockRepository } from "@test/mocks/repository"
describe("ProductService", () => {
let productService: ProductService
let mockProductRepository: MockRepository
let mockVariantRepository: MockRepository
beforeEach(() => {
mockProductRepository = new MockRepository()
mockVariantRepository = new MockRepository()
productService = new ProductService({
productRepository: mockProductRepository,
variantRepository: mockVariantRepository
})
})
describe("createProduct", () => {
it("should create a product with valid data", async () => {
const productData = {
title: "Test Product",
description: "Test description",
handle: "test-product",
status: "published"
}
const expectedProduct = {
id: "prod_1",
...productData,
created_at: new Date(),
updated_at: new Date()
}
mockProductRepository.create.mockResolvedValue(expectedProduct)
mockProductRepository.save.mockResolvedValue(expectedProduct)
const result = await productService.createProduct(productData)
expect(mockProductRepository.create).toHaveBeenCalledWith(productData)
expect(mockProductRepository.save).toHaveBeenCalledWith(expectedProduct)
expect(result).toEqual(expectedProduct)
})
it("should throw error for duplicate handle", async () => {
const productData = {
title: "Test Product",
handle: "existing-handle"
}
mockProductRepository.findOne.mockResolvedValue({ id: "existing_prod" })
await expect(productService.createProduct(productData))
.rejects
.toThrow("Product with handle 'existing-handle' already exists")
})
it("should validate required fields", async () => {
const invalidData = { description: "Missing title" }
await expect(productService.createProduct(invalidData as any))
.rejects
.toThrow("Title is required")
})
})
describe("updateProduct", () => {
it("should update existing product", async () => {
const productId = "prod_1"
const updateData = { title: "Updated Title" }
const existingProduct = {
id: productId,
title: "Original Title",
handle: "test-product"
}
const updatedProduct = { ...existingProduct, ...updateData }
mockProductRepository.findOne.mockResolvedValue(existingProduct)
mockProductRepository.save.mockResolvedValue(updatedProduct)
const result = await productService.updateProduct(productId, updateData)
expect(result.title).toBe("Updated Title")
expect(mockProductRepository.save).toHaveBeenCalledWith(updatedProduct)
})
it("should throw error for non-existent product", async () => {
const productId = "non_existent"
mockProductRepository.findOne.mockResolvedValue(null)
await expect(productService.updateProduct(productId, {}))
.rejects
.toThrow("Product not found")
})
})
describe("searchProducts", () => {
it("should search products by title", async () => {
const searchTerm = "test"
const mockProducts = [
{ id: "prod_1", title: "Test Product 1" },
{ id: "prod_2", title: "Test Product 2" }
]
mockProductRepository.findAndCount.mockResolvedValue([mockProducts, 2])
const result = await productService.searchProducts(searchTerm)
expect(result.products).toHaveLength(2)
expect(result.count).toBe(2)
expect(mockProductRepository.findAndCount).toHaveBeenCalledWith({
where: [
{ title: { contains: searchTerm } },
{ description: { contains: searchTerm } }
],
take: 20,
skip: 0
})
})
})
})
API Route Testing
// __tests__/api/products/route.test.ts
import { GET, POST } from "@/api/products/route"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
describe("Products API", () => {
let mockReq: Partial<MedusaRequest>
let mockRes: Partial<MedusaResponse>
let mockProductService: any
beforeEach(() => {
mockProductService = {
listProducts: jest.fn(),
createProduct: jest.fn(),
retrieveProduct: jest.fn()
}
mockReq = {
scope: {
resolve: jest.fn().mockReturnValue(mockProductService)
},
query: {},
body: {},
params: {}
}
mockRes = {
json: jest.fn(),
status: jest.fn().mockReturnThis(),
send: jest.fn()
}
})
describe("GET /products", () => {
it("should return products list", async () => {
const mockProducts = [
{ id: "prod_1", title: "Product 1" },
{ id: "prod_2", title: "Product 2" }
]
mockProductService.listProducts.mockResolvedValue(mockProducts)
await GET(mockReq as MedusaRequest, mockRes as MedusaResponse)
expect(mockRes.json).toHaveBeenCalledWith({
products: mockProducts
})
})
it("should handle query parameters", async () => {
mockReq.query = { limit: "10", offset: "5", q: "test" }
mockProductService.listProducts.mockResolvedValue([])
await GET(mockReq as MedusaRequest, mockRes as MedusaResponse)
expect(mockProductService.listProducts).toHaveBeenCalledWith({
limit: 10,
offset: 5,
q: "test"
})
})
it("should handle service errors", async () => {
mockProductService.listProducts.mockRejectedValue(new Error("Database error"))
await GET(mockReq as MedusaRequest, mockRes as MedusaResponse)
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Internal server error",
message: "Database error"
})
})
})
describe("POST /products", () => {
it("should create a new product", async () => {
const productData = {
title: "New Product",
description: "Product description",
handle: "new-product"
}
const createdProduct = { id: "prod_new", ...productData }
mockReq.body = productData
mockProductService.createProduct.mockResolvedValue(createdProduct)
await POST(mockReq as MedusaRequest, mockRes as MedusaResponse)
expect(mockProductService.createProduct).toHaveBeenCalledWith(productData)
expect(mockRes.status).toHaveBeenCalledWith(201)
expect(mockRes.json).toHaveBeenCalledWith({
product: createdProduct
})
})
it("should validate request body", async () => {
mockReq.body = { description: "Missing title" }
await POST(mockReq as MedusaRequest, mockRes as MedusaResponse)
expect(mockRes.status).toHaveBeenCalledWith(400)
expect(mockRes.json).toHaveBeenCalledWith({
error: "Validation error",
details: expect.any(Array)
})
})
})
})
Integration Testing
Database Integration Tests
// __tests__/integration/product.test.ts
import { initializeTestDatabase, cleanupTestDatabase } from "@test/helpers/database"
import { createTestApplication } from "@test/helpers/application"
import { ProductService } from "@/modules/product/services/product"
describe("Product Integration Tests", () => {
let app: any
let productService: ProductService
beforeAll(async () => {
await initializeTestDatabase()
app = await createTestApplication()
productService = app.container.resolve("productService")
})
afterAll(async () => {
await cleanupTestDatabase()
})
beforeEach(async () => {
await app.database.query("TRUNCATE TABLE product CASCADE")
})
it("should create and retrieve product with variants", async () => {
// Create product
const product = await productService.createProduct({
title: "Integration Test Product",
description: "Test product for integration testing",
handle: "integration-test-product"
})
expect(product.id).toBeDefined()
expect(product.title).toBe("Integration Test Product")
// Create variants
const variant1 = await productService.createProductVariant(product.id, {
title: "Small",
sku: "INT-TEST-SM",
prices: [{ currency_code: "usd", amount: 2000 }]
})
const variant2 = await productService.createProductVariant(product.id, {
title: "Large",
sku: "INT-TEST-LG",
prices: [{ currency_code: "usd", amount: 2500 }]
})
// Retrieve product with variants
const retrievedProduct = await productService.retrieveProduct(product.id, {
relations: ["variants", "variants.prices"]
})
expect(retrievedProduct.variants).toHaveLength(2)
expect(retrievedProduct.variants[0].sku).toMatch(/INT-TEST-(SM|LG)/)
expect(retrievedProduct.variants[0].prices).toHaveLength(1)
})
it("should handle product search correctly", async () => {
// Create test products
await Promise.all([
productService.createProduct({
title: "Red Shirt",
description: "A red cotton shirt",
handle: "red-shirt"
}),
productService.createProduct({
title: "Blue Shirt",
description: "A blue cotton shirt",
handle: "blue-shirt"
}),
productService.createProduct({
title: "Red Pants",
description: "Red cotton pants",
handle: "red-pants"
})
])
// Search for "red" products
const redProducts = await productService.searchProducts("red")
expect(redProducts.products).toHaveLength(2)
// Search for "shirt" products
const shirtProducts = await productService.searchProducts("shirt")
expect(shirtProducts.products).toHaveLength(2)
// Search for "blue" products
const blueProducts = await productService.searchProducts("blue")
expect(blueProducts.products).toHaveLength(1)
})
})
API Integration Tests
// __tests__/integration/api/products.test.ts
import request from "supertest"
import { createTestApplication } from "@test/helpers/application"
describe("Products API Integration", () => {
let app: any
beforeAll(async () => {
app = await createTestApplication()
})
beforeEach(async () => {
await app.database.query("TRUNCATE TABLE product CASCADE")
})
describe("Product CRUD Operations", () => {
it("should complete full product lifecycle", async () => {
// Create product
const createResponse = await request(app)
.post("/api/products")
.send({
title: "API Test Product",
description: "Product created via API test",
handle: "api-test-product"
})
.expect(201)
const productId = createResponse.body.product.id
expect(productId).toBeDefined()
// Retrieve product
const getResponse = await request(app)
.get(`/api/products/${productId}`)
.expect(200)
expect(getResponse.body.product.title).toBe("API Test Product")
// Update product
const updateResponse = await request(app)
.put(`/api/products/${productId}`)
.send({
title: "Updated API Test Product",
status: "published"
})
.expect(200)
expect(updateResponse.body.product.title).toBe("Updated API Test Product")
expect(updateResponse.body.product.status).toBe("published")
// List products
const listResponse = await request(app)
.get("/api/products")
.expect(200)
expect(listResponse.body.products).toHaveLength(1)
expect(listResponse.body.products[0].id).toBe(productId)
// Delete product
await request(app)
.delete(`/api/products/${productId}`)
.expect(204)
// Verify deletion
await request(app)
.get(`/api/products/${productId}`)
.expect(404)
})
it("should handle validation errors", async () => {
const response = await request(app)
.post("/api/products")
.send({
description: "Product without title"
})
.expect(400)
expect(response.body.error).toContain("validation")
})
it("should search products correctly", async () => {
// Create test products
await Promise.all([
request(app)
.post("/api/products")
.send({
title: "Red T-Shirt",
handle: "red-tshirt"
}),
request(app)
.post("/api/products")
.send({
title: "Blue T-Shirt",
handle: "blue-tshirt"
})
])
// Search for T-Shirt
const searchResponse = await request(app)
.get("/api/products?q=T-Shirt")
.expect(200)
expect(searchResponse.body.products).toHaveLength(2)
})
})
})
Mock Utilities
Repository Mocks
// test/mocks/repository.ts
export class MockRepository {
find = jest.fn()
findOne = jest.fn()
findAndCount = jest.fn()
create = jest.fn()
save = jest.fn()
update = jest.fn()
delete = jest.fn()
remove = jest.fn()
count = jest.fn()
reset() {
Object.values(this).forEach(mock => {
if (typeof mock === 'function' && 'mockClear' in mock) {
mock.mockClear()
}
})
}
}
Service Mocks
// test/mocks/services.ts
export const createMockProductService = () => ({
listProducts: jest.fn(),
retrieveProduct: jest.fn(),
createProduct: jest.fn(),
updateProduct: jest.fn(),
deleteProduct: jest.fn(),
searchProducts: jest.fn(),
createProductVariant: jest.fn(),
updateProductVariant: jest.fn(),
deleteProductVariant: jest.fn()
})
export const createMockOrderService = () => ({
listOrders: jest.fn(),
retrieveOrder: jest.fn(),
createOrder: jest.fn(),
updateOrder: jest.fn(),
cancelOrder: jest.fn(),
createFulfillment: jest.fn(),
createPayment: jest.fn()
})
export const createMockCustomerService = () => ({
listCustomers: jest.fn(),
retrieveCustomer: jest.fn(),
createCustomer: jest.fn(),
updateCustomer: jest.fn(),
deleteCustomer: jest.fn(),
addCustomerToGroup: jest.fn(),
removeCustomerFromGroup: jest.fn()
})
Test Helpers
// test/helpers/database.ts
import { DataSource } from "typeorm"
let testDataSource: DataSource
export async function initializeTestDatabase() {
testDataSource = new DataSource({
type: "sqlite",
database: ":memory:",
entities: ["src/**/*.entity.ts"],
synchronize: true,
logging: false
})
await testDataSource.initialize()
return testDataSource
}
export async function cleanupTestDatabase() {
if (testDataSource?.isInitialized) {
await testDataSource.destroy()
}
}
export function getTestDataSource() {
return testDataSource
}
// test/helpers/application.ts
import { createMedusaApp } from "@medusajs/framework"
export async function createTestApplication() {
const app = await createMedusaApp({
database: {
type: "sqlite",
database: ":memory:",
synchronize: true
},
redis: {
host: "localhost",
port: 6379,
db: 1 // Use different DB for tests
}
})
await app.initialize()
return app
}
Performance Testing
Load Testing
// __tests__/performance/products.test.ts
describe("Product Performance Tests", () => {
let app: any
let productService: any
beforeAll(async () => {
app = await createTestApplication()
productService = app.container.resolve("productService")
})
it("should handle bulk product creation efficiently", async () => {
const startTime = Date.now()
const promises = Array.from({ length: 100 }, (_, index) =>
productService.createProduct({
title: `Performance Test Product ${index}`,
handle: `performance-test-${index}`,
description: "Bulk created product for performance testing"
})
)
await Promise.all(promises)
const endTime = Date.now()
const duration = endTime - startTime
console.log(`Created 100 products in ${duration}ms`)
expect(duration).toBeLessThan(5000) // Should complete in under 5 seconds
})
it("should search large product catalog efficiently", async () => {
// Create large dataset
await Promise.all(
Array.from({ length: 1000 }, (_, index) =>
productService.createProduct({
title: `Product ${index}`,
handle: `product-${index}`,
description: `Description for product ${index}`
})
)
)
const startTime = Date.now()
const results = await productService.searchProducts("Product", {
limit: 50,
offset: 0
})
const endTime = Date.now()
const duration = endTime - startTime
console.log(`Searched 1000 products in ${duration}ms`)
expect(duration).toBeLessThan(1000) // Should complete in under 1 second
expect(results.products).toHaveLength(50)
})
})
Memory Testing
// __tests__/performance/memory.test.ts
describe("Memory Usage Tests", () => {
it("should not have memory leaks in product operations", async () => {
const getMemoryUsage = () => process.memoryUsage().heapUsed
const initialMemory = getMemoryUsage()
// Perform many operations
for (let i = 0; i < 1000; i++) {
await productService.createProduct({
title: `Memory Test Product ${i}`,
handle: `memory-test-${i}`
})
await productService.deleteProduct(`memory-test-${i}`)
}
// Force garbage collection
if (global.gc) global.gc()
const finalMemory = getMemoryUsage()
const memoryIncrease = finalMemory - initialMemory
console.log(`Memory increase: ${memoryIncrease / 1024 / 1024} MB`)
// Memory increase should be minimal
expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024) // 50 MB
})
})