Files
puaros/packages/guardian/tests/AnemicModelDetector.test.ts

373 lines
9.0 KiB
TypeScript

import { describe, it, expect, beforeEach } from "vitest"
import { AnemicModelDetector } from "../src/infrastructure/analyzers/AnemicModelDetector"
describe("AnemicModelDetector", () => {
let detector: AnemicModelDetector
beforeEach(() => {
detector = new AnemicModelDetector()
})
describe("detectAnemicModels", () => {
it("should detect class with only getters and setters", () => {
const code = `
class Order {
private status: string
private total: number
getStatus(): string {
return this.status
}
setStatus(status: string): void {
this.status = status
}
getTotal(): number {
return this.total
}
setTotal(total: number): void {
this.total = total
}
}
`
const violations = detector.detectAnemicModels(
code,
"src/domain/entities/Order.ts",
"domain",
)
expect(violations).toHaveLength(1)
expect(violations[0].className).toBe("Order")
expect(violations[0].methodCount).toBeGreaterThan(0)
expect(violations[0].propertyCount).toBeGreaterThan(0)
expect(violations[0].getMessage()).toContain("Order")
})
it("should detect class with public setters", () => {
const code = `
class User {
private email: string
private password: string
public setEmail(email: string): void {
this.email = email
}
public getEmail(): string {
return this.email
}
public setPassword(password: string): void {
this.password = password
}
}
`
const violations = detector.detectAnemicModels(
code,
"src/domain/entities/User.ts",
"domain",
)
expect(violations).toHaveLength(1)
expect(violations[0].className).toBe("User")
expect(violations[0].hasPublicSetters).toBe(true)
})
it("should not detect rich domain model with business logic", () => {
const code = `
class Order {
private readonly id: string
private status: OrderStatus
private items: OrderItem[]
public approve(): void {
if (!this.canBeApproved()) {
throw new Error("Cannot approve")
}
this.status = OrderStatus.APPROVED
}
public reject(reason: string): void {
if (!this.canBeRejected()) {
throw new Error("Cannot reject")
}
this.status = OrderStatus.REJECTED
}
public addItem(item: OrderItem): void {
if (this.isApproved()) {
throw new Error("Cannot modify approved order")
}
this.items.push(item)
}
public calculateTotal(): Money {
return this.items.reduce((sum, item) => sum.add(item.getPrice()), Money.zero())
}
public getStatus(): OrderStatus {
return this.status
}
private canBeApproved(): boolean {
return this.status === OrderStatus.PENDING
}
private canBeRejected(): boolean {
return this.status === OrderStatus.PENDING
}
private isApproved(): boolean {
return this.status === OrderStatus.APPROVED
}
}
`
const violations = detector.detectAnemicModels(
code,
"src/domain/entities/Order.ts",
"domain",
)
expect(violations).toHaveLength(0)
})
it("should not analyze files outside domain layer", () => {
const code = `
class OrderDto {
getStatus(): string {
return this.status
}
setStatus(status: string): void {
this.status = status
}
}
`
const violations = detector.detectAnemicModels(
code,
"src/application/dtos/OrderDto.ts",
"application",
)
expect(violations).toHaveLength(0)
})
it("should not analyze DTO files", () => {
const code = `
class UserDto {
private email: string
getEmail(): string {
return this.email
}
setEmail(email: string): void {
this.email = email
}
}
`
const violations = detector.detectAnemicModels(
code,
"src/domain/dtos/UserDto.ts",
"domain",
)
expect(violations).toHaveLength(0)
})
it("should not analyze test files", () => {
const code = `
class Order {
getStatus(): string {
return this.status
}
setStatus(status: string): void {
this.status = status
}
}
`
const violations = detector.detectAnemicModels(
code,
"src/domain/entities/Order.test.ts",
"domain",
)
expect(violations).toHaveLength(0)
})
it("should detect anemic model in entities folder", () => {
const code = `
class Product {
private name: string
private price: number
getName(): string {
return this.name
}
setName(name: string): void {
this.name = name
}
getPrice(): number {
return this.price
}
setPrice(price: number): void {
this.price = price
}
}
`
const violations = detector.detectAnemicModels(
code,
"src/domain/entities/Product.ts",
"domain",
)
expect(violations).toHaveLength(1)
expect(violations[0].className).toBe("Product")
})
it("should detect anemic model in aggregates folder", () => {
const code = `
class Customer {
private email: string
getEmail(): string {
return this.email
}
setEmail(email: string): void {
this.email = email
}
}
`
const violations = detector.detectAnemicModels(
code,
"src/domain/aggregates/customer/Customer.ts",
"domain",
)
expect(violations).toHaveLength(1)
expect(violations[0].className).toBe("Customer")
})
it("should not detect class with good method-to-property ratio", () => {
const code = `
class Account {
private balance: number
private isActive: boolean
public deposit(amount: number): void {
if (amount <= 0) throw new Error("Invalid amount")
this.balance += amount
}
public withdraw(amount: number): void {
if (amount > this.balance) throw new Error("Insufficient funds")
this.balance -= amount
}
public activate(): void {
this.isActive = true
}
public deactivate(): void {
this.isActive = false
}
public getBalance(): number {
return this.balance
}
}
`
const violations = detector.detectAnemicModels(
code,
"src/domain/entities/Account.ts",
"domain",
)
expect(violations).toHaveLength(0)
})
it("should handle class with no properties or methods", () => {
const code = `
class EmptyEntity {
}
`
const violations = detector.detectAnemicModels(
code,
"src/domain/entities/EmptyEntity.ts",
"domain",
)
expect(violations).toHaveLength(0)
})
it("should detect multiple anemic classes in one file", () => {
const code = `
class Order {
getStatus() { return this.status }
setStatus(status: string) { this.status = status }
}
class Item {
getPrice() { return this.price }
setPrice(price: number) { this.price = price }
}
`
const violations = detector.detectAnemicModels(
code,
"src/domain/entities/Models.ts",
"domain",
)
expect(violations).toHaveLength(2)
expect(violations[0].className).toBe("Order")
expect(violations[1].className).toBe("Item")
})
it("should provide correct violation details", () => {
const code = `
class Payment {
private amount: number
private currency: string
getAmount(): number {
return this.amount
}
setAmount(amount: number): void {
this.amount = amount
}
getCurrency(): string {
return this.currency
}
setCurrency(currency: string): void {
this.currency = currency
}
}
`
const violations = detector.detectAnemicModels(
code,
"src/domain/entities/Payment.ts",
"domain",
)
expect(violations).toHaveLength(1)
const violation = violations[0]
expect(violation.className).toBe("Payment")
expect(violation.filePath).toBe("src/domain/entities/Payment.ts")
expect(violation.layer).toBe("domain")
expect(violation.line).toBeGreaterThan(0)
expect(violation.getMessage()).toContain("Payment")
expect(violation.getSuggestion()).toContain("business")
})
})
})