Trong một thế giới lý tưởng, bạn sẽ viết bằng chứng thay vì kiểm tra. Ví dụ, hãy xem xét các chức năng sau.
const negate = (x: number): number => -x;
const reverse = (x: string): string => x.split("").reverse().join("");
const transform = (x: number|string): number|string => {
switch (typeof x) {
case "number": return negate(x);
case "string": return reverse(x);
}
};
Giả sử bạn muốn chứng minh rằng transform
áp dụng hai lần là idempotent , tức là đối với tất cả các đầu vào hợp lệ x
, transform(transform(x))
đều bằng x
. Chà, trước tiên bạn sẽ cần phải chứng minh điều đó negate
và reverse
áp dụng hai lần là không cần thiết. Bây giờ, giả sử rằng việc chứng minh tính không thay đổi negate
và reverse
được áp dụng hai lần là không đáng kể, tức là trình biên dịch có thể tìm ra nó. Như vậy, chúng ta có những bổ đề sau .
const negateNegateIdempotent = (x: number): negate(negate(x))≡x => refl;
const reverseReverseIdempotent = (x: string): reverse(reverse(x))≡x => refl;
Chúng ta có thể sử dụng hai bổ đề này để chứng minh rằng đó transform
là idempotent như sau.
const transformTransformIdempotent = (x: number|string): transform(transform(x))≡x => {
switch (typeof x) {
case "number": return negateNegateIdempotent(x);
case "string": return reverseReverseIdempotent(x);
}
};
Có rất nhiều thứ đang diễn ra ở đây, vì vậy hãy phá vỡ nó.
- Cũng giống như
a|b
một loại kết hợp và a&b
là một loại giao nhau, a≡b
là một loại bình đẳng.
- Một giá trị
x
của một loại bằng a≡b
là một bằng chứng về sự bằng nhau của a
và b
.
- Nếu hai giá trị
a
và b
không bằng nhau thì không thể tạo giá trị loại a≡b
.
- Giá trị
refl
, viết tắt của tính phản xạ , có loại a≡a
. Đó là bằng chứng tầm thường của một giá trị tương đương với chính nó.
- Chúng tôi sử dụng
refl
trong bằng chứng của negateNegateIdempotent
và reverseReverseIdempotent
. Điều này là có thể bởi vì các mệnh đề đủ tầm thường để trình biên dịch tự động chứng minh.
- Chúng tôi sử dụng
negateNegateIdempotent
và reverseReverseIdempotent
bổ đề để chứng minh transformTransformIdempotent
. Đây là một ví dụ về một bằng chứng không tầm thường.
Ưu điểm của việc viết bằng chứng là trình biên dịch xác minh bằng chứng. Nếu bằng chứng không chính xác, thì chương trình không gõ được kiểm tra và trình biên dịch sẽ báo lỗi. Bằng chứng là tốt hơn so với các xét nghiệm vì hai lý do. Đầu tiên, bạn không phải tạo dữ liệu thử nghiệm. Thật khó để tạo dữ liệu thử nghiệm xử lý tất cả các trường hợp cạnh. Thứ hai, bạn sẽ không vô tình quên kiểm tra bất kỳ trường hợp cạnh. Trình biên dịch sẽ đưa ra một lỗi nếu bạn làm.
Thật không may, TypeScript không có loại bằng vì nó không hỗ trợ các loại phụ thuộc, tức là các loại phụ thuộc vào các giá trị. Do đó, bạn không thể viết bằng chứng trong TypeScript. Bạn có thể viết bằng chứng bằng các ngôn ngữ lập trình chức năng phụ thuộc như Agda .
Tuy nhiên, bạn có thể viết các mệnh đề trong TypeScript.
const negateNegateIdempotent = (x: number): boolean => negate(negate(x)) === x;
const reverseReverseIdempotent = (x: string): boolean => reverse(reverse(x)) === x;
const transformTransformIdempotent = (x: number|string): boolean => {
switch (typeof x) {
case "number": return negateNegateIdempotent(x);
case "string": return reverseReverseIdempotent(x);
}
};
Sau đó, bạn có thể sử dụng một thư viện như jsverify để tự động tạo dữ liệu thử nghiệm cho nhiều trường hợp thử nghiệm.
const jsc = require("jsverify");
jsc.assert(jsc.forall("number", transformTransformIdempotent)); // OK, passed 100 tests
jsc.assert(jsc.forall("string", transformTransformIdempotent)); // OK, passed 100 tests
Bạn cũng có thể gọi jsc.forall
với "number | string"
nhưng tôi dường như không thể làm cho nó hoạt động.
Vì vậy, để trả lời câu hỏi của bạn.
Làm thế nào một người nên đi kiểm tra foo()
?
Lập trình chức năng khuyến khích thử nghiệm dựa trên tài sản. Ví dụ, tôi đã thử nghiệm negate
, reverse
và transform
các chức năng áp dụng hai lần cho idempotence. Nếu bạn theo dõi kiểm tra dựa trên thuộc tính, thì các hàm đề xuất của bạn sẽ có cấu trúc tương tự như các chức năng mà bạn đang kiểm tra.
Bạn có nên coi thực tế là nó ủy nhiệm fnForString()
và fnForNumber()
như một chi tiết triển khai và về cơ bản sao chép các bài kiểm tra cho từng bài kiểm tra khi viết bài kiểm tra cho foo()
? Sự lặp lại này có được chấp nhận không?
Vâng, có thể chấp nhận được. Mặc dù, bạn hoàn toàn có thể từ bỏ thử nghiệm fnForString
và fnForNumber
bởi vì các thử nghiệm cho những thử nghiệm được bao gồm trong các thử nghiệm cho foo
. Tuy nhiên, để hoàn thiện tôi sẽ khuyên bạn nên bao gồm tất cả các thử nghiệm ngay cả khi nó giới thiệu dự phòng.
Bạn có nên viết các bài kiểm tra "biết" mà foo()
ủy nhiệm fnForString()
và fnForNumber()
ví dụ bằng cách chế nhạo chúng và kiểm tra xem nó có ủy nhiệm cho chúng không?
Các đề xuất mà bạn viết trong kiểm tra dựa trên thuộc tính tuân theo cấu trúc của các chức năng bạn đang kiểm tra. Do đó, họ "biết" về các phụ thuộc bằng cách sử dụng các mệnh đề của các chức năng khác đang được thử nghiệm. Không cần phải chế nhạo họ. Bạn chỉ cần chế giễu những thứ như cuộc gọi mạng, cuộc gọi hệ thống tệp, v.v.