140 lines
5.5 KiB
TypeScript
140 lines
5.5 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
||
import * as fc from 'fast-check'
|
||
import { calculateRing, validateInput } from './calculator'
|
||
import type { RingCalculatorInput } from '../types/calculator'
|
||
|
||
// 生成有效的非负数输入
|
||
const validInputArb: fc.Arbitrary<RingCalculatorInput> = fc.record({
|
||
goldWeight: fc.float({ min: 0, max: 1000, noNaN: true }),
|
||
mainStoneWeight: fc.float({ min: 0, max: 100, noNaN: true }),
|
||
sideStoneWeight: fc.float({ min: 0, max: 100, noNaN: true }),
|
||
lossRate: fc.float({ min: 0, max: 10, noNaN: true }),
|
||
moldGoldPrice: fc.float({ min: 0, max: 10000, noNaN: true }),
|
||
mainStoneUnitPrice: fc.float({ min: 0, max: 100000, noNaN: true }),
|
||
sideStoneUnitPrice: fc.float({ min: 0, max: 100000, noNaN: true }),
|
||
sideStoneCount: fc.integer({ min: 0, max: 1000 }),
|
||
microSettingFee: fc.float({ min: 0, max: 1000, noNaN: true }),
|
||
mainStoneSettingFee: fc.float({ min: 0, max: 100000, noNaN: true }),
|
||
threeDFee: fc.float({ min: 0, max: 100000, noNaN: true }),
|
||
basicLaborCost: fc.float({ min: 0, max: 100000, noNaN: true }),
|
||
otherCost: fc.float({ min: 0, max: 100000, noNaN: true }),
|
||
})
|
||
|
||
// Feature: jewelry-mall, Property 4: 钻戒计算器公式正确性
|
||
// **Validates: Requirements 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8**
|
||
describe('Property 4: 钻戒计算器公式正确性', () => {
|
||
it('所有输出字段应符合设计文档公式', () => {
|
||
fc.assert(
|
||
fc.property(validInputArb, (input) => {
|
||
const result = calculateRing(input)
|
||
|
||
// 4.2 净金重 = 金重 - 主石重×0.2 - 副石重×0.2
|
||
const expectedNetGold = input.goldWeight - input.mainStoneWeight * 0.2 - input.sideStoneWeight * 0.2
|
||
expect(result.netGoldWeight).toBeCloseTo(expectedNetGold, 10)
|
||
|
||
// 4.3 含耗重 = 净金重 × 损耗
|
||
const expectedWeightWithLoss = expectedNetGold * input.lossRate
|
||
expect(result.weightWithLoss).toBeCloseTo(expectedWeightWithLoss, 10)
|
||
|
||
// 4.4 金值 = 含耗重 × 倒模金价
|
||
const expectedGoldValue = expectedWeightWithLoss * input.moldGoldPrice
|
||
expect(result.goldValue).toBeCloseTo(expectedGoldValue, 5)
|
||
|
||
// 4.5 主石总价 = 主石重 × 主石单价
|
||
const expectedMainStone = input.mainStoneWeight * input.mainStoneUnitPrice
|
||
expect(result.mainStoneTotal).toBeCloseTo(expectedMainStone, 10)
|
||
|
||
// 4.6 副石总价 = 副石重 × 副石单价
|
||
const expectedSideStone = input.sideStoneWeight * input.sideStoneUnitPrice
|
||
expect(result.sideStoneTotal).toBeCloseTo(expectedSideStone, 10)
|
||
|
||
// 4.7 微镶总价 = 副石粒数 × 微镶费
|
||
const expectedMicroSetting = input.sideStoneCount * input.microSettingFee
|
||
expect(result.microSettingTotal).toBeCloseTo(expectedMicroSetting, 10)
|
||
|
||
// 4.8 总价 = 金值 + 主石总价 + 副石总价 + 主石镶费 + 微镶总价 + 3D起板费 + 基本工费 + 其他费用
|
||
const expectedTotal = expectedGoldValue + expectedMainStone + expectedSideStone
|
||
+ input.mainStoneSettingFee + expectedMicroSetting
|
||
+ input.threeDFee + input.basicLaborCost + input.otherCost
|
||
expect(result.totalPrice).toBeCloseTo(expectedTotal, 5)
|
||
}),
|
||
{ numRuns: 100 },
|
||
)
|
||
})
|
||
})
|
||
|
||
// 单元测试:边界情况和非法输入
|
||
// Requirements: 4.2-4.8
|
||
describe('calculateRing 单元测试', () => {
|
||
const zeroInput: RingCalculatorInput = {
|
||
goldWeight: 0,
|
||
mainStoneWeight: 0,
|
||
sideStoneWeight: 0,
|
||
lossRate: 0,
|
||
moldGoldPrice: 0,
|
||
mainStoneUnitPrice: 0,
|
||
sideStoneUnitPrice: 0,
|
||
sideStoneCount: 0,
|
||
microSettingFee: 0,
|
||
mainStoneSettingFee: 0,
|
||
threeDFee: 0,
|
||
basicLaborCost: 0,
|
||
otherCost: 0,
|
||
}
|
||
|
||
it('所有输入为 0 时,所有输出应为 0', () => {
|
||
const result = calculateRing(zeroInput)
|
||
expect(result.netGoldWeight).toBe(0)
|
||
expect(result.weightWithLoss).toBe(0)
|
||
expect(result.goldValue).toBe(0)
|
||
expect(result.mainStoneTotal).toBe(0)
|
||
expect(result.sideStoneTotal).toBe(0)
|
||
expect(result.microSettingTotal).toBe(0)
|
||
expect(result.totalPrice).toBe(0)
|
||
})
|
||
|
||
it('仅金重有值时,净金重等于金重', () => {
|
||
const input = { ...zeroInput, goldWeight: 10, lossRate: 1.05, moldGoldPrice: 500 }
|
||
const result = calculateRing(input)
|
||
expect(result.netGoldWeight).toBe(10)
|
||
expect(result.weightWithLoss).toBeCloseTo(10.5, 10)
|
||
expect(result.goldValue).toBeCloseTo(5250, 5)
|
||
expect(result.totalPrice).toBeCloseTo(5250, 5)
|
||
})
|
||
|
||
it('极大值输入不应抛出异常', () => {
|
||
const input: RingCalculatorInput = {
|
||
goldWeight: 999999,
|
||
mainStoneWeight: 999999,
|
||
sideStoneWeight: 999999,
|
||
lossRate: 999999,
|
||
moldGoldPrice: 999999,
|
||
mainStoneUnitPrice: 999999,
|
||
sideStoneUnitPrice: 999999,
|
||
sideStoneCount: 999999,
|
||
microSettingFee: 999999,
|
||
mainStoneSettingFee: 999999,
|
||
threeDFee: 999999,
|
||
basicLaborCost: 999999,
|
||
otherCost: 999999,
|
||
}
|
||
expect(() => calculateRing(input)).not.toThrow()
|
||
})
|
||
|
||
it('负数金重应被拒绝', () => {
|
||
expect(() => calculateRing({ ...zeroInput, goldWeight: -1 })).toThrow('金重不能为负数')
|
||
})
|
||
|
||
it('负数主石重应被拒绝', () => {
|
||
expect(() => calculateRing({ ...zeroInput, mainStoneWeight: -0.5 })).toThrow('主石重不能为负数')
|
||
})
|
||
|
||
it('负数损耗应被拒绝', () => {
|
||
expect(() => calculateRing({ ...zeroInput, lossRate: -1 })).toThrow('损耗不能为负数')
|
||
})
|
||
|
||
it('负数其他费用应被拒绝', () => {
|
||
expect(() => calculateRing({ ...zeroInput, otherCost: -100 })).toThrow('其他费用不能为负数')
|
||
})
|
||
})
|