Kiểm tra chức năng thuần túy trên loại kết hợp mà ủy nhiệm cho các chức năng thuần túy khác


8

Giả sử bạn có hàm lấy một loại kết hợp và sau đó thu hẹp loại và ủy quyền cho một trong hai hàm thuần túy khác.

function foo(arg: string|number) {
    if (typeof arg === 'string') {
        return fnForString(arg)
    } else {
        return fnForNumber(arg)
    }
}

Giả sử rằng fnForString()fnForNumber()cũng là các hàm thuần túy, và chúng đã được kiểm tra.

Làm thế nào một người nên đi kiểm tra foo()?

  • Bạn có nên coi thực tế là nó ủy nhiệm fnForString()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?
  • Bạn có nên viết các bài kiểm tra "biết" mà foo()ủy nhiệm fnForString()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?

2
Bạn đã tạo ra hàm quá tải của riêng mình với các phụ thuộc được mã hóa cứng, Một cách khác để đạt được loại đa hình này (chính xác là đa hình ad-hoc) là chuyển các phụ thuộc hàm dưới dạng đối số (một thư mục kiểu). Sau đó, bạn có thể sử dụng các hàm giả cho mục đích thử nghiệm.
bob

Ok, nhưng đó là một trường hợp "làm thế nào để đạt được sự chế giễu" - vì vậy, bạn có thể chuyển các chức năng vào hoặc có chức năng bị vênh, v.v. Nhưng câu hỏi của tôi là ở mức độ "làm thế nào để chế giễu?" nhưng đúng hơn là "đang chế giễu cách tiếp cận đúng trong bối cảnh của các hàm thuần túy?".
lấy mẫu

Câu trả lời:


1

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 đó negatereverseá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 negatereverseđượ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 đó transformlà 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ó.

  1. Cũng giống như a|bmột loại kết hợp và a&blà một loại giao nhau, a≡blà một loại bình đẳng.
  2. Một giá trị xcủa một loại bằng a≡blà một bằng chứng về sự bằng nhau của ab.
  3. Nếu hai giá trị abkhông bằng nhau thì không thể tạo giá trị loại a≡b.
  4. 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ó.
  5. Chúng tôi sử dụng refltrong bằng chứng của negateNegateIdempotentreverseReverseIdempotent. Đ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.
  6. Chúng tôi sử dụng negateNegateIdempotentreverseReverseIdempotentbổ đề để 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.forallvớ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, reversetransformcá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()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 fnForStringfnForNumberbở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()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.


5

Giải pháp tốt nhất sẽ chỉ là thử nghiệm cho foo.

fnForStringfnForNumberlà một chi tiết triển khai mà bạn có thể thay đổi trong tương lai mà không nhất thiết phải thay đổi hành vi của foo. Nếu điều đó xảy ra, các bài kiểm tra của bạn có thể bị hỏng mà không có lý do, loại vấn đề này làm cho bài kiểm tra của bạn quá rộng rãi và vô dụng.

Giao diện của bạn chỉ cần foo, chỉ cần kiểm tra cho nó.

Nếu bạn phải kiểm tra fnForStringfnForNumbergiữ loại thử nghiệm này ngoài các thử nghiệm giao diện công cộng của bạn.

Đây là cách giải thích của tôi về nguyên tắc sau đây của Kent Beck

Kiểm tra lập trình viên nên nhạy cảm với những thay đổi hành vi và không nhạy cảm với những thay đổi cấu trúc. Nếu hành vi của chương trình ổn định theo quan điểm của người quan sát, không có thử nghiệm nào sẽ thay đổi.


2

Câu trả lời ngắn: đặc điểm kỹ thuật của hàm xác định cách thức mà nó cần được kiểm tra.

Câu trả lời dài:

Kiểm tra = sử dụng một tập hợp các trường hợp kiểm thử (hy vọng đại diện cho tất cả các trường hợp có thể gặp phải) để xác minh rằng việc triển khai đáp ứng đặc điểm kỹ thuật của nó.

Trong ví dụ foo được nêu mà không có đặc điểm kỹ thuật, vì vậy người ta nên kiểm tra foo bằng cách không làm gì cả (hoặc nhiều nhất là một số thử nghiệm ngớ ngẩn để xác minh yêu cầu ngầm định rằng "foo chấm dứt theo cách này hay cách khác").

Nếu đặc tả là một cái gì đó hoạt động như "hàm này trả về kết quả của việc áp dụng các đối số cho fnForString hoặc fnForNumber theo loại đối số" thì việc chế nhạo các đại biểu (tùy chọn 2) là cách để đi. Bất kể điều gì xảy ra với fnForString / Number, foo vẫn tuân theo thông số kỹ thuật của nó.

Nếu đặc tả không phụ thuộc vào fnForType theo cách như vậy thì sử dụng lại các thử nghiệm cho fnFortype (tùy chọn 1) là cách tốt nhất (giả sử các thử nghiệm đó là tốt).

Lưu ý rằng các thông số kỹ thuật vận hành loại bỏ phần lớn quyền tự do thông thường để thay thế một triển khai bằng một triển khai khác (một cách thanh lịch hơn / dễ đọc / hiệu quả / vv). Chúng chỉ nên được sử dụng sau khi xem xét cẩn thận.


1

Giả sử rằng fnForString () và fnForNumber () cũng là các hàm thuần túy và chúng đã được kiểm tra.

Cũng kể từ khi chi tiết thực hiện được giao cho fnForString()fnForNumber()cho stringnumbertương ứng, thử nghiệm nó nắm để chỉ đơn thuần là đảm bảo rằng foocác cuộc gọi chức năng đúng. Vì vậy, có, tôi sẽ chế giễu họ và đảm bảo rằng họ được gọi phù hợp.

foo("a string")
fnForNumberMock.hasNotBeenCalled()
fnForStringMock.hasBeenCalled()

fnForString()fnForNumber()đã được kiểm tra riêng lẻ, bạn biết rằng khi bạn gọi foo(), nó gọi đúng chức năng và bạn biết chức năng này thực hiện những gì nó phải làm.

foo nên trả lại một cái gì đó. Bạn có thể trả lại một cái gì đó từ giả của mình, mỗi thứ khác nhau và đảm bảo rằng foo trả về chính xác (ví dụ: nếu bạn quên một hàm fooreturn của mình ).

Và tất cả mọi thứ đã được bảo hiểm.


0

Tôi nghĩ rằng việc kiểm tra loại chức năng của bạn là vô ích, hệ thống có thể làm điều này một mình và cho phép bạn đặt cùng tên cho từng loại đối tượng mà bạn quan tâm

mã mẫu

  //  fnForStringorNumber String Wrapper  
String.prototype.fnForStringorNumber = function() {
  return  this.repeat(3)
}
  //  fnForStringorNumber Number Wrapper  
Number.prototype.fnForStringorNumber = function() {
  return this *3
}

function foo( arg ) {
  return arg.fnForStringorNumber(4321)
}

console.log ( foo(1234) )       // 3702
console.log ( foo('abcd_') )   // abcd_abcd_abcd_

// or simply:
console.log ( (12).fnForStringorNumber() )     // 36
console.log ( 'xyz_'.fnForStringorNumber() )   // xyz_xyz_xyz_

Tôi có thể không phải là một nhà lý thuyết tuyệt vời về các kỹ thuật mã hóa, nhưng tôi đã bảo trì rất nhiều mã. Tôi nghĩ rằng người ta thực sự có thể đánh giá tính hiệu quả của cách mã hóa chỉ trong các trường hợp cụ thể, việc đầu cơ không thể có giá trị bằng chứng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.