@qlover/create-app 0.10.5 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -627,7 +627,20 @@ describe('AsyncService', () => {
|
|
|
627
627
|
|
|
628
628
|
### Type Safety Testing
|
|
629
629
|
|
|
630
|
+
TypeScript project tests should not only verify runtime behavior but also ensure the correctness of the type system. Vitest provides the `expectTypeOf` utility for compile-time type checking.
|
|
631
|
+
|
|
632
|
+
#### Why Type Testing?
|
|
633
|
+
|
|
634
|
+
1. **Type Inference Validation**: Ensure TypeScript correctly infers complex types
|
|
635
|
+
2. **Generic Constraint Checking**: Verify generic parameter constraints
|
|
636
|
+
3. **Type Compatibility**: Ensure type definitions match actual usage
|
|
637
|
+
4. **API Contract Guarantee**: Prevent breaking changes in type definitions
|
|
638
|
+
|
|
639
|
+
#### Basic Type Testing
|
|
640
|
+
|
|
630
641
|
```typescript
|
|
642
|
+
import { describe, it, expectTypeOf } from 'vitest';
|
|
643
|
+
|
|
631
644
|
describe('TypeSafetyTests', () => {
|
|
632
645
|
it('should maintain type safety', () => {
|
|
633
646
|
const processor = new DataProcessor<User>();
|
|
@@ -636,9 +649,190 @@ describe('TypeSafetyTests', () => {
|
|
|
636
649
|
expectTypeOf(processor.process).parameter(0).toEqualTypeOf<User>();
|
|
637
650
|
expectTypeOf(processor.process).returns.toEqualTypeOf<ProcessedUser>();
|
|
638
651
|
});
|
|
652
|
+
|
|
653
|
+
it('should infer correct return types', () => {
|
|
654
|
+
const result = getData();
|
|
655
|
+
|
|
656
|
+
// Verify return type
|
|
657
|
+
expectTypeOf(result).toEqualTypeOf<{ id: number; name: string }>();
|
|
658
|
+
expectTypeOf(result).not.toEqualTypeOf<{ id: string; name: string }>();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('should validate parameter types', () => {
|
|
662
|
+
function processUser(user: User): void {
|
|
663
|
+
// implementation
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Verify parameter type
|
|
667
|
+
expectTypeOf(processUser).parameter(0).toMatchTypeOf<{ id: number }>();
|
|
668
|
+
expectTypeOf(processUser).parameter(0).toHaveProperty('id');
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
#### Generic Type Testing
|
|
674
|
+
|
|
675
|
+
```typescript
|
|
676
|
+
describe('Generic Type Tests', () => {
|
|
677
|
+
it('should work with generic constraints', () => {
|
|
678
|
+
class Storage<T extends { id: number }> {
|
|
679
|
+
store(item: T): T {
|
|
680
|
+
return item;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const storage = new Storage<User>();
|
|
685
|
+
|
|
686
|
+
// Verify generic type
|
|
687
|
+
expectTypeOf(storage.store).parameter(0).toMatchTypeOf<User>();
|
|
688
|
+
expectTypeOf(storage.store).returns.toMatchTypeOf<User>();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('should validate complex generic types', () => {
|
|
692
|
+
type ApiResponse<T> = {
|
|
693
|
+
data: T;
|
|
694
|
+
status: number;
|
|
695
|
+
message?: string;
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const response: ApiResponse<User[]> = {
|
|
699
|
+
data: [],
|
|
700
|
+
status: 200
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// Verify nested generic type
|
|
704
|
+
expectTypeOf(response).toMatchTypeOf<ApiResponse<User[]>>();
|
|
705
|
+
expectTypeOf(response.data).toEqualTypeOf<User[]>();
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
#### Union and Intersection Type Testing
|
|
711
|
+
|
|
712
|
+
```typescript
|
|
713
|
+
describe('Union and Intersection Types', () => {
|
|
714
|
+
it('should handle union types correctly', () => {
|
|
715
|
+
type Result = Success | Error;
|
|
716
|
+
type Success = { status: 'success'; data: string };
|
|
717
|
+
type Error = { status: 'error'; message: string };
|
|
718
|
+
|
|
719
|
+
function handleResult(result: Result): void {
|
|
720
|
+
// implementation
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Verify union type
|
|
724
|
+
expectTypeOf(handleResult).parameter(0).toMatchTypeOf<Success>();
|
|
725
|
+
expectTypeOf(handleResult).parameter(0).toMatchTypeOf<Error>();
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('should handle intersection types correctly', () => {
|
|
729
|
+
type Timestamped = { createdAt: Date; updatedAt: Date };
|
|
730
|
+
type UserWithTimestamp = User & Timestamped;
|
|
731
|
+
|
|
732
|
+
const user: UserWithTimestamp = {
|
|
733
|
+
id: 1,
|
|
734
|
+
name: 'John',
|
|
735
|
+
createdAt: new Date(),
|
|
736
|
+
updatedAt: new Date()
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// Verify intersection type contains all properties
|
|
740
|
+
expectTypeOf(user).toHaveProperty('id');
|
|
741
|
+
expectTypeOf(user).toHaveProperty('name');
|
|
742
|
+
expectTypeOf(user).toHaveProperty('createdAt');
|
|
743
|
+
expectTypeOf(user).toHaveProperty('updatedAt');
|
|
744
|
+
});
|
|
639
745
|
});
|
|
640
746
|
```
|
|
641
747
|
|
|
748
|
+
#### Type Narrowing Testing
|
|
749
|
+
|
|
750
|
+
```typescript
|
|
751
|
+
describe('Type Narrowing Tests', () => {
|
|
752
|
+
it('should validate type guards', () => {
|
|
753
|
+
function isString(value: unknown): value is string {
|
|
754
|
+
return typeof value === 'string';
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const value: unknown = 'test';
|
|
758
|
+
|
|
759
|
+
if (isString(value)) {
|
|
760
|
+
// Within this scope, value should be narrowed to string type
|
|
761
|
+
expectTypeOf(value).toEqualTypeOf<string>();
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('should validate discriminated unions', () => {
|
|
766
|
+
type Shape =
|
|
767
|
+
| { kind: 'circle'; radius: number }
|
|
768
|
+
| { kind: 'rectangle'; width: number; height: number };
|
|
769
|
+
|
|
770
|
+
function getArea(shape: Shape): number {
|
|
771
|
+
if (shape.kind === 'circle') {
|
|
772
|
+
// In this branch, shape should be narrowed to circle type
|
|
773
|
+
expectTypeOf(shape).toHaveProperty('radius');
|
|
774
|
+
expectTypeOf(shape).not.toHaveProperty('width');
|
|
775
|
+
return Math.PI * shape.radius ** 2;
|
|
776
|
+
} else {
|
|
777
|
+
// In this branch, shape should be narrowed to rectangle type
|
|
778
|
+
expectTypeOf(shape).toHaveProperty('width');
|
|
779
|
+
expectTypeOf(shape).toHaveProperty('height');
|
|
780
|
+
return shape.width * shape.height;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
#### Practical Recommendations
|
|
788
|
+
|
|
789
|
+
1. **Combine with Runtime Tests**: Type tests should complement runtime tests
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
describe('Combined Runtime and Type Tests', () => {
|
|
793
|
+
it('should validate both runtime behavior and types', () => {
|
|
794
|
+
function add(a: number, b: number): number {
|
|
795
|
+
return a + b;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Type test
|
|
799
|
+
expectTypeOf(add).parameter(0).toEqualTypeOf<number>();
|
|
800
|
+
expectTypeOf(add).returns.toEqualTypeOf<number>();
|
|
801
|
+
|
|
802
|
+
// Runtime test
|
|
803
|
+
expect(add(1, 2)).toBe(3);
|
|
804
|
+
expect(add(-1, 1)).toBe(0);
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
2. **Test Type Inference**: Ensure TypeScript correctly infers types, avoid overusing `any`
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
describe('Type Inference Tests', () => {
|
|
813
|
+
it('should infer types correctly', () => {
|
|
814
|
+
const data = { id: 1, name: 'John' };
|
|
815
|
+
|
|
816
|
+
// Verify inferred type
|
|
817
|
+
expectTypeOf(data).toEqualTypeOf<{ id: number; name: string }>();
|
|
818
|
+
expectTypeOf(data.id).toEqualTypeOf<number>();
|
|
819
|
+
expectTypeOf(data.name).toEqualTypeOf<string>();
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
3. **Use TypeScript Compiler Checks**: Run `tsc --noEmit` in CI to ensure no type errors
|
|
825
|
+
|
|
826
|
+
```bash
|
|
827
|
+
# Add script in package.json
|
|
828
|
+
{
|
|
829
|
+
"scripts": {
|
|
830
|
+
"type-check": "tsc --noEmit",
|
|
831
|
+
"test": "pnpm type-check && vitest run"
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
642
836
|
---
|
|
643
837
|
|
|
644
838
|
## Performance Testing
|
|
@@ -627,7 +627,20 @@ describe('AsyncService', () => {
|
|
|
627
627
|
|
|
628
628
|
### 类型安全测试
|
|
629
629
|
|
|
630
|
+
TypeScript 项目的测试不仅要验证运行时行为,还应该确保类型系统的正确性。Vitest 提供了 `expectTypeOf` 工具来进行编译时类型检查。
|
|
631
|
+
|
|
632
|
+
#### 为什么需要类型测试?
|
|
633
|
+
|
|
634
|
+
1. **类型推断验证**:确保 TypeScript 能正确推断复杂类型
|
|
635
|
+
2. **泛型约束检查**:验证泛型参数的约束条件
|
|
636
|
+
3. **类型兼容性**:确保类型定义与实际使用匹配
|
|
637
|
+
4. **API 契约保证**:防止类型定义的破坏性变更
|
|
638
|
+
|
|
639
|
+
#### 基础类型测试
|
|
640
|
+
|
|
630
641
|
```typescript
|
|
642
|
+
import { describe, it, expectTypeOf } from 'vitest';
|
|
643
|
+
|
|
631
644
|
describe('TypeSafetyTests', () => {
|
|
632
645
|
it('should maintain type safety', () => {
|
|
633
646
|
const processor = new DataProcessor<User>();
|
|
@@ -636,9 +649,190 @@ describe('TypeSafetyTests', () => {
|
|
|
636
649
|
expectTypeOf(processor.process).parameter(0).toEqualTypeOf<User>();
|
|
637
650
|
expectTypeOf(processor.process).returns.toEqualTypeOf<ProcessedUser>();
|
|
638
651
|
});
|
|
652
|
+
|
|
653
|
+
it('should infer correct return types', () => {
|
|
654
|
+
const result = getData();
|
|
655
|
+
|
|
656
|
+
// 验证返回值类型
|
|
657
|
+
expectTypeOf(result).toEqualTypeOf<{ id: number; name: string }>();
|
|
658
|
+
expectTypeOf(result).not.toEqualTypeOf<{ id: string; name: string }>();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('should validate parameter types', () => {
|
|
662
|
+
function processUser(user: User): void {
|
|
663
|
+
// implementation
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 验证参数类型
|
|
667
|
+
expectTypeOf(processUser).parameter(0).toMatchTypeOf<{ id: number }>();
|
|
668
|
+
expectTypeOf(processUser).parameter(0).toHaveProperty('id');
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
#### 泛型类型测试
|
|
674
|
+
|
|
675
|
+
```typescript
|
|
676
|
+
describe('Generic Type Tests', () => {
|
|
677
|
+
it('should work with generic constraints', () => {
|
|
678
|
+
class Storage<T extends { id: number }> {
|
|
679
|
+
store(item: T): T {
|
|
680
|
+
return item;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const storage = new Storage<User>();
|
|
685
|
+
|
|
686
|
+
// 验证泛型类型
|
|
687
|
+
expectTypeOf(storage.store).parameter(0).toMatchTypeOf<User>();
|
|
688
|
+
expectTypeOf(storage.store).returns.toMatchTypeOf<User>();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('should validate complex generic types', () => {
|
|
692
|
+
type ApiResponse<T> = {
|
|
693
|
+
data: T;
|
|
694
|
+
status: number;
|
|
695
|
+
message?: string;
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const response: ApiResponse<User[]> = {
|
|
699
|
+
data: [],
|
|
700
|
+
status: 200
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// 验证嵌套泛型类型
|
|
704
|
+
expectTypeOf(response).toMatchTypeOf<ApiResponse<User[]>>();
|
|
705
|
+
expectTypeOf(response.data).toEqualTypeOf<User[]>();
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
#### 联合类型与交叉类型测试
|
|
711
|
+
|
|
712
|
+
```typescript
|
|
713
|
+
describe('Union and Intersection Types', () => {
|
|
714
|
+
it('should handle union types correctly', () => {
|
|
715
|
+
type Result = Success | Error;
|
|
716
|
+
type Success = { status: 'success'; data: string };
|
|
717
|
+
type Error = { status: 'error'; message: string };
|
|
718
|
+
|
|
719
|
+
function handleResult(result: Result): void {
|
|
720
|
+
// implementation
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// 验证联合类型
|
|
724
|
+
expectTypeOf(handleResult).parameter(0).toMatchTypeOf<Success>();
|
|
725
|
+
expectTypeOf(handleResult).parameter(0).toMatchTypeOf<Error>();
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('should handle intersection types correctly', () => {
|
|
729
|
+
type Timestamped = { createdAt: Date; updatedAt: Date };
|
|
730
|
+
type UserWithTimestamp = User & Timestamped;
|
|
731
|
+
|
|
732
|
+
const user: UserWithTimestamp = {
|
|
733
|
+
id: 1,
|
|
734
|
+
name: 'John',
|
|
735
|
+
createdAt: new Date(),
|
|
736
|
+
updatedAt: new Date()
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// 验证交叉类型包含所有属性
|
|
740
|
+
expectTypeOf(user).toHaveProperty('id');
|
|
741
|
+
expectTypeOf(user).toHaveProperty('name');
|
|
742
|
+
expectTypeOf(user).toHaveProperty('createdAt');
|
|
743
|
+
expectTypeOf(user).toHaveProperty('updatedAt');
|
|
744
|
+
});
|
|
639
745
|
});
|
|
640
746
|
```
|
|
641
747
|
|
|
748
|
+
#### 类型窄化测试
|
|
749
|
+
|
|
750
|
+
```typescript
|
|
751
|
+
describe('Type Narrowing Tests', () => {
|
|
752
|
+
it('should validate type guards', () => {
|
|
753
|
+
function isString(value: unknown): value is string {
|
|
754
|
+
return typeof value === 'string';
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const value: unknown = 'test';
|
|
758
|
+
|
|
759
|
+
if (isString(value)) {
|
|
760
|
+
// 在此作用域内,value 应该被窄化为 string 类型
|
|
761
|
+
expectTypeOf(value).toEqualTypeOf<string>();
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('should validate discriminated unions', () => {
|
|
766
|
+
type Shape =
|
|
767
|
+
| { kind: 'circle'; radius: number }
|
|
768
|
+
| { kind: 'rectangle'; width: number; height: number };
|
|
769
|
+
|
|
770
|
+
function getArea(shape: Shape): number {
|
|
771
|
+
if (shape.kind === 'circle') {
|
|
772
|
+
// 在此分支,shape 应该被窄化为 circle 类型
|
|
773
|
+
expectTypeOf(shape).toHaveProperty('radius');
|
|
774
|
+
expectTypeOf(shape).not.toHaveProperty('width');
|
|
775
|
+
return Math.PI * shape.radius ** 2;
|
|
776
|
+
} else {
|
|
777
|
+
// 在此分支,shape 应该被窄化为 rectangle 类型
|
|
778
|
+
expectTypeOf(shape).toHaveProperty('width');
|
|
779
|
+
expectTypeOf(shape).toHaveProperty('height');
|
|
780
|
+
return shape.width * shape.height;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
#### 实用建议
|
|
788
|
+
|
|
789
|
+
1. **结合运行时测试**:类型测试应该与运行时测试相辅相成
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
describe('Combined Runtime and Type Tests', () => {
|
|
793
|
+
it('should validate both runtime behavior and types', () => {
|
|
794
|
+
function add(a: number, b: number): number {
|
|
795
|
+
return a + b;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// 类型测试
|
|
799
|
+
expectTypeOf(add).parameter(0).toEqualTypeOf<number>();
|
|
800
|
+
expectTypeOf(add).returns.toEqualTypeOf<number>();
|
|
801
|
+
|
|
802
|
+
// 运行时测试
|
|
803
|
+
expect(add(1, 2)).toBe(3);
|
|
804
|
+
expect(add(-1, 1)).toBe(0);
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
2. **测试类型推断**:确保 TypeScript 能正确推断类型,避免过度使用 `any`
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
describe('Type Inference Tests', () => {
|
|
813
|
+
it('should infer types correctly', () => {
|
|
814
|
+
const data = { id: 1, name: 'John' };
|
|
815
|
+
|
|
816
|
+
// 验证推断的类型
|
|
817
|
+
expectTypeOf(data).toEqualTypeOf<{ id: number; name: string }>();
|
|
818
|
+
expectTypeOf(data.id).toEqualTypeOf<number>();
|
|
819
|
+
expectTypeOf(data.name).toEqualTypeOf<string>();
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
3. **使用 TypeScript 编译器检查**:在 CI 中运行 `tsc --noEmit` 确保没有类型错误
|
|
825
|
+
|
|
826
|
+
```bash
|
|
827
|
+
# 在 package.json 中添加脚本
|
|
828
|
+
{
|
|
829
|
+
"scripts": {
|
|
830
|
+
"type-check": "tsc --noEmit",
|
|
831
|
+
"test": "pnpm type-check && vitest run"
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
642
836
|
---
|
|
643
837
|
|
|
644
838
|
## 性能测试
|