dashboard.service.property.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. import * as fc from 'fast-check';
  2. /**
  3. * **Feature: backend-frontend-integration, Property 7: Dashboard Stats Accuracy**
  4. * **Validates: Requirements 6.1**
  5. *
  6. * *For any* dashboard stats query, the returned todayOrderCount SHALL equal the count
  7. * of orders created today, and todayGMV SHALL equal the sum of totalAmount for
  8. * completed orders today.
  9. */
  10. // 模拟订单数据结构
  11. interface MockOrder {
  12. id: string;
  13. orderNo: string;
  14. sellerId: string;
  15. totalAmount: number;
  16. status: 'PendingPayment' | 'PendingShipment' | 'Shipped' | 'Completed' | 'Cancelled';
  17. createdAt: Date;
  18. }
  19. // 模拟今日统计结果
  20. interface TodayStats {
  21. todayOrderCount: number;
  22. todayGMV: number;
  23. pendingShipmentCount: number;
  24. }
  25. /**
  26. * 纯函数:计算今日统计数据
  27. * 用于属性测试验证业务逻辑的正确性
  28. */
  29. function calculateTodayStats(orders: MockOrder[], sellerId: string, today: Date): TodayStats {
  30. // 设置今日开始时间
  31. const todayStart = new Date(today);
  32. todayStart.setHours(0, 0, 0, 0);
  33. // 过滤该商家的订单
  34. const sellerOrders = orders.filter(order => order.sellerId === sellerId);
  35. // 今日订单:创建时间在今日的订单
  36. const todayOrders = sellerOrders.filter(order => order.createdAt >= todayStart);
  37. const todayOrderCount = todayOrders.length;
  38. // 今日GMV:今日已完成订单的总额
  39. const todayCompletedOrders = todayOrders.filter(order => order.status === 'Completed');
  40. const todayGMV = todayCompletedOrders.reduce((sum, order) => sum + order.totalAmount, 0);
  41. // 待发货订单数:所有待发货订单(不限于今日)
  42. const pendingShipmentCount = sellerOrders.filter(
  43. order => order.status === 'PendingShipment'
  44. ).length;
  45. return {
  46. todayOrderCount,
  47. todayGMV,
  48. pendingShipmentCount
  49. };
  50. }
  51. /**
  52. * 辅助函数:生成指定日期范围内的随机日期
  53. */
  54. function randomDateInRange(start: Date, end: Date): Date {
  55. const startTime = start.getTime();
  56. const endTime = end.getTime();
  57. const randomTime = startTime + Math.random() * (endTime - startTime);
  58. return new Date(randomTime);
  59. }
  60. describe('DashboardService Property Tests', () => {
  61. // 生成订单状态的arbitrary
  62. const orderStatusArb = fc.constantFrom(
  63. 'PendingPayment' as const,
  64. 'PendingShipment' as const,
  65. 'Shipped' as const,
  66. 'Completed' as const,
  67. 'Cancelled' as const
  68. );
  69. describe('Property 7: Dashboard Stats Accuracy', () => {
  70. it('今日订单数等于今日创建的订单数量', () => {
  71. fc.assert(
  72. fc.property(
  73. fc.uuid(), // sellerId
  74. fc.integer({ min: 0, max: 50 }), // 订单数量
  75. (sellerId, orderCount) => {
  76. const today = new Date();
  77. const todayStart = new Date(today);
  78. todayStart.setHours(0, 0, 0, 0);
  79. // 生成订单:一半今日,一半昨日
  80. const orders: MockOrder[] = [];
  81. const yesterday = new Date(today);
  82. yesterday.setDate(yesterday.getDate() - 1);
  83. for (let i = 0; i < orderCount; i++) {
  84. const isToday = i % 2 === 0;
  85. const createdAt = isToday
  86. ? randomDateInRange(todayStart, today)
  87. : randomDateInRange(yesterday, todayStart);
  88. orders.push({
  89. id: `order-${i}`,
  90. orderNo: `ORDER${String(i).padStart(14, '0')}`,
  91. sellerId: i % 3 === 0 ? 'other-seller' : sellerId, // 1/3是其他商家
  92. totalAmount: Math.random() * 1000,
  93. status: 'Completed',
  94. createdAt
  95. });
  96. }
  97. const stats = calculateTodayStats(orders, sellerId, today);
  98. // 手动计算预期的今日订单数
  99. const expectedTodayCount = orders.filter(
  100. o => o.sellerId === sellerId && o.createdAt >= todayStart
  101. ).length;
  102. return stats.todayOrderCount === expectedTodayCount;
  103. }
  104. ),
  105. { numRuns: 100 }
  106. );
  107. });
  108. it('今日GMV等于今日已完成订单的总额', () => {
  109. fc.assert(
  110. fc.property(
  111. fc.uuid(), // sellerId
  112. fc.array(
  113. fc.record({
  114. totalAmount: fc.integer({ min: 100, max: 100000 }).map(cents => cents / 100),
  115. status: orderStatusArb,
  116. isToday: fc.boolean(),
  117. isSeller: fc.boolean()
  118. }),
  119. { minLength: 0, maxLength: 30 }
  120. ),
  121. (sellerId, orderData) => {
  122. const today = new Date();
  123. const todayStart = new Date(today);
  124. todayStart.setHours(0, 0, 0, 0);
  125. const yesterday = new Date(today);
  126. yesterday.setDate(yesterday.getDate() - 1);
  127. // 根据生成的数据创建订单
  128. const orders: MockOrder[] = orderData.map((data, i) => ({
  129. id: `order-${i}`,
  130. orderNo: `ORDER${String(i).padStart(14, '0')}`,
  131. sellerId: data.isSeller ? sellerId : 'other-seller',
  132. totalAmount: data.totalAmount,
  133. status: data.status,
  134. createdAt: data.isToday
  135. ? randomDateInRange(todayStart, today)
  136. : randomDateInRange(yesterday, todayStart)
  137. }));
  138. const stats = calculateTodayStats(orders, sellerId, today);
  139. // 手动计算预期的今日GMV
  140. const expectedGMV = orders
  141. .filter(o =>
  142. o.sellerId === sellerId &&
  143. o.createdAt >= todayStart &&
  144. o.status === 'Completed'
  145. )
  146. .reduce((sum, o) => sum + o.totalAmount, 0);
  147. // 使用近似比较处理浮点数精度问题
  148. return Math.abs(stats.todayGMV - expectedGMV) < 0.01;
  149. }
  150. ),
  151. { numRuns: 100 }
  152. );
  153. });
  154. it('待发货订单数等于状态为PendingShipment的订单数量', () => {
  155. fc.assert(
  156. fc.property(
  157. fc.uuid(), // sellerId
  158. fc.array(
  159. fc.record({
  160. status: orderStatusArb,
  161. isSeller: fc.boolean()
  162. }),
  163. { minLength: 0, maxLength: 30 }
  164. ),
  165. (sellerId, orderData) => {
  166. const today = new Date();
  167. const orders: MockOrder[] = orderData.map((data, i) => ({
  168. id: `order-${i}`,
  169. orderNo: `ORDER${String(i).padStart(14, '0')}`,
  170. sellerId: data.isSeller ? sellerId : 'other-seller',
  171. totalAmount: Math.random() * 1000,
  172. status: data.status,
  173. createdAt: today
  174. }));
  175. const stats = calculateTodayStats(orders, sellerId, today);
  176. // 手动计算预期的待发货订单数
  177. const expectedPending = orders.filter(
  178. o => o.sellerId === sellerId && o.status === 'PendingShipment'
  179. ).length;
  180. return stats.pendingShipmentCount === expectedPending;
  181. }
  182. ),
  183. { numRuns: 100 }
  184. );
  185. });
  186. it('空订单列表返回零统计', () => {
  187. fc.assert(
  188. fc.property(
  189. fc.uuid(),
  190. (sellerId) => {
  191. const today = new Date();
  192. const stats = calculateTodayStats([], sellerId, today);
  193. return (
  194. stats.todayOrderCount === 0 &&
  195. stats.todayGMV === 0 &&
  196. stats.pendingShipmentCount === 0
  197. );
  198. }
  199. ),
  200. { numRuns: 100 }
  201. );
  202. });
  203. it('不同商家的统计数据相互独立', () => {
  204. fc.assert(
  205. fc.property(
  206. fc.uuid(),
  207. fc.uuid(),
  208. fc.array(
  209. fc.record({
  210. totalAmount: fc.integer({ min: 100, max: 100000 }).map(cents => cents / 100),
  211. status: orderStatusArb
  212. }),
  213. { minLength: 1, maxLength: 20 }
  214. ),
  215. (sellerId1, sellerId2, orderData) => {
  216. // 确保两个商家ID不同
  217. if (sellerId1 === sellerId2) return true;
  218. const today = new Date();
  219. const todayStart = new Date(today);
  220. todayStart.setHours(0, 0, 0, 0);
  221. // 创建订单,分配给两个商家
  222. const orders: MockOrder[] = orderData.map((data, i) => ({
  223. id: `order-${i}`,
  224. orderNo: `ORDER${String(i).padStart(14, '0')}`,
  225. sellerId: i % 2 === 0 ? sellerId1 : sellerId2,
  226. totalAmount: data.totalAmount,
  227. status: data.status,
  228. createdAt: randomDateInRange(todayStart, today)
  229. }));
  230. const stats1 = calculateTodayStats(orders, sellerId1, today);
  231. const stats2 = calculateTodayStats(orders, sellerId2, today);
  232. // 验证:两个商家的订单数之和等于总订单数
  233. const totalOrders = orders.filter(o => o.createdAt >= todayStart).length;
  234. return stats1.todayOrderCount + stats2.todayOrderCount === totalOrders;
  235. }
  236. ),
  237. { numRuns: 100 }
  238. );
  239. });
  240. it('GMV只计算已完成订单,不包括其他状态', () => {
  241. fc.assert(
  242. fc.property(
  243. fc.uuid(),
  244. fc.integer({ min: 100, max: 10000 }).map(cents => cents / 100),
  245. (sellerId, amount) => {
  246. const today = new Date();
  247. const todayStart = new Date(today);
  248. todayStart.setHours(0, 0, 0, 0);
  249. // 创建不同状态的订单,金额相同
  250. const statuses: Array<'PendingPayment' | 'PendingShipment' | 'Shipped' | 'Completed' | 'Cancelled'> = [
  251. 'PendingPayment',
  252. 'PendingShipment',
  253. 'Shipped',
  254. 'Completed',
  255. 'Cancelled'
  256. ];
  257. const orders: MockOrder[] = statuses.map((status, i) => ({
  258. id: `order-${i}`,
  259. orderNo: `ORDER${String(i).padStart(14, '0')}`,
  260. sellerId,
  261. totalAmount: amount,
  262. status,
  263. createdAt: randomDateInRange(todayStart, today)
  264. }));
  265. const stats = calculateTodayStats(orders, sellerId, today);
  266. // GMV应该只等于一个已完成订单的金额
  267. return Math.abs(stats.todayGMV - amount) < 0.01;
  268. }
  269. ),
  270. { numRuns: 100 }
  271. );
  272. });
  273. it('昨日订单不计入今日统计', () => {
  274. fc.assert(
  275. fc.property(
  276. fc.uuid(),
  277. fc.integer({ min: 1, max: 20 }),
  278. (sellerId, orderCount) => {
  279. const today = new Date();
  280. const todayStart = new Date(today);
  281. todayStart.setHours(0, 0, 0, 0);
  282. const yesterday = new Date(today);
  283. yesterday.setDate(yesterday.getDate() - 1);
  284. const yesterdayStart = new Date(yesterday);
  285. yesterdayStart.setHours(0, 0, 0, 0);
  286. // 只创建昨日的订单
  287. const orders: MockOrder[] = [];
  288. for (let i = 0; i < orderCount; i++) {
  289. orders.push({
  290. id: `order-${i}`,
  291. orderNo: `ORDER${String(i).padStart(14, '0')}`,
  292. sellerId,
  293. totalAmount: Math.random() * 1000,
  294. status: 'Completed',
  295. createdAt: randomDateInRange(yesterdayStart, todayStart)
  296. });
  297. }
  298. const stats = calculateTodayStats(orders, sellerId, today);
  299. // 今日订单数和GMV应该都是0
  300. return stats.todayOrderCount === 0 && stats.todayGMV === 0;
  301. }
  302. ),
  303. { numRuns: 100 }
  304. );
  305. });
  306. });
  307. });