Làm thế nào để làm TDD cho một cái gì đó với nhiều hoán vị?


15

Khi tạo một hệ thống như AI, có thể thực hiện nhiều đường dẫn khác nhau rất nhanh hoặc thực sự là bất kỳ thuật toán nào có nhiều đầu vào khác nhau, tập kết quả có thể có thể chứa một số lượng lớn hoán vị.

Cách tiếp cận nào nên sử dụng TDD khi tạo một hệ thống tạo ra nhiều, nhiều hoán vị kết quả khác nhau?


1
Độ tốt tổng thể của hệ thống AI thường được đo bằng phép thử Chính xác-Thu hồi với bộ đầu vào chuẩn. Bài kiểm tra này gần ngang với "bài kiểm tra tích hợp". Như những người khác đã đề cập, nó giống như "nghiên cứu thuật toán dựa trên thử nghiệm" hơn là " thiết kế dựa trên thử nghiệm ".
rwong

Vui lòng xác định những gì bạn có nghĩa là "AI". Đó là một lĩnh vực nghiên cứu nhiều hơn bất kỳ loại chương trình cụ thể. Đối với việc triển khai AI nhất định, bạn thường không thể kiểm tra một số loại điều (ví dụ: hành vi mới nổi) thông qua TDD.
Steven Evers

@SnOrfus Ý tôi là nó theo nghĩa chung nhất, thô sơ nhất, một cỗ máy ra quyết định.
Nicole

Câu trả lời:


7

Thực hiện một cách tiếp cận thực tế hơn cho câu trả lời của pdr . TDD là tất cả về thiết kế phần mềm hơn là thử nghiệm. Bạn sử dụng các bài kiểm tra đơn vị để xác minh công việc của bạn khi bạn đi cùng.

Vì vậy, ở cấp độ kiểm tra đơn vị, bạn cần thiết kế các đơn vị để chúng có thể được kiểm tra theo cách hoàn toàn xác định. Bạn có thể làm điều này bằng cách lấy bất cứ thứ gì làm cho đơn vị không xác định (chẳng hạn như trình tạo số ngẫu nhiên) và trừu tượng đi. Giả sử chúng ta có một ví dụ ngây thơ về một phương pháp quyết định xem một nước đi có tốt hay không:

class Decider {

  public boolean decide(float input, float risk) {

      float inputRand = Math.random();
      if (inputRand > input) {
         float riskRand = Math.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider();
d.decide(0.1337f, 0.1337f);

Phương pháp này rất khó kiểm tra và điều duy nhất bạn thực sự có thể xác minh trong các bài kiểm tra đơn vị là giới hạn của nó ... nhưng điều đó đòi hỏi rất nhiều cố gắng để đi đến giới hạn. Vì vậy, thay vào đó, hãy trừu tượng hóa phần ngẫu nhiên bằng cách tạo giao diện và lớp cụ thể bao bọc chức năng:

public interface IRandom {

   public float random();

}

public class ConcreteRandom implements IRandom {

   public float random() {
      return Math.random();
   }

}

Các Deciderlớp học hiện nay cần phải sử dụng lớp bê tông qua trừu tượng của nó, tức là giao diện. Cách làm này được gọi là tiêm phụ thuộc (ví dụ dưới đây là một ví dụ về tiêm xây dựng, nhưng bạn cũng có thể làm điều này với một setter):

class Decider {

  IRandom irandom;

  public Decider(IRandom irandom) { // constructor injection
      this.irandom = irandom;
  }

  public boolean decide(float input, float risk) {

      float inputRand = irandom.random();
      if (inputRand > input) {
         float riskRand = irandom.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider(new ConcreteRandom);
d.decide(0.1337f, 0.1337f);

Bạn có thể tự hỏi tại sao "mã phình" này là cần thiết. Vâng, đối với người mới bắt đầu, bây giờ bạn có thể chế giễu hành vi của phần ngẫu nhiên của thuật toán bởi vì Deciderbây giờ có một phụ thuộc tuân theo IRandom"hợp đồng". Bạn có thể sử dụng khung mô phỏng cho việc này, nhưng ví dụ này đủ đơn giản để tự viết mã:

class MockedRandom() implements IRandom {

    public List<Float> floats = new ArrayList<Float>();
    int pos;

   public void addFloat(float f) {
     floats.add(f);
   }

   public float random() {
      float out = floats.get(pos);
      if (pos != floats.size()) {
         pos++;
      }
      return out;
   }

}

Phần tốt nhất là điều này có thể thay thế hoàn toàn việc thực hiện cụ thể "thực tế". Mã trở nên dễ dàng để kiểm tra như thế này:

@Before void setUp() {
  MockedRandom mRandom = new MockedRandom();

  Decider decider = new Decider(mRandom);
}

@Test
public void testDecisionWithLowInput_ShouldGiveFalse() {

  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandButLowRiskRand_ShouldGiveFalse() {

  mRandom.addFloat(1f);
  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandAndHighRiskRand_ShouldGiveTrue() {

  mRandom.addFloat(1f);
  mRandom.addFloat(1f);

  assertTrue(decider.decide(0.1337f, 0.1337f));
}

Hy vọng điều này cung cấp cho bạn ý tưởng về cách thiết kế ứng dụng của bạn để các hoán vị có thể bị ép buộc để bạn có thể kiểm tra tất cả các trường hợp cạnh và không có gì.


3

TDD nghiêm ngặt có xu hướng phá vỡ một chút đối với các hệ thống phức tạp hơn, nhưng điều đó không quá quan trọng về mặt thực tế - một khi bạn vượt ra ngoài khả năng cô lập các đầu vào riêng lẻ, chỉ cần chọn một số trường hợp thử nghiệm cung cấp phạm vi bảo hiểm hợp lý và sử dụng chúng.

Điều này đòi hỏi một số kiến ​​thức về việc triển khai sẽ làm tốt điều gì, nhưng đó là vấn đề quan tâm hơn về mặt lý thuyết - bạn rất khó có thể xây dựng một AI được người dùng phi kỹ thuật chỉ định chi tiết. Nó cùng loại với việc vượt qua các bài kiểm tra bằng cách mã hóa cứng cho các trường hợp thử nghiệm - chính thức thử nghiệm là thông số kỹ thuật và việc thực hiện vừa chính xác vừa là giải pháp nhanh nhất có thể, nhưng thực tế không bao giờ xảy ra.


2

TDD không phải là về thử nghiệm, mà là về thiết kế.

Khác xa với sự phức tạp, nó vượt trội trong những trường hợp này. Nó sẽ thúc đẩy bạn xem xét vấn đề lớn hơn trong các phần nhỏ hơn, điều này sẽ dẫn đến một thiết kế tốt hơn.

Đừng đặt ra để thử kiểm tra mọi hoán vị của thuật toán của bạn. Chỉ cần xây dựng thử nghiệm sau khi thử nghiệm, viết mã đơn giản nhất để làm cho thử nghiệm hoạt động, cho đến khi bạn có cơ sở của bạn được bảo hiểm. Bạn nên hiểu ý tôi nói về việc giải quyết vấn đề vì bạn sẽ được khuyến khích giả mạo các phần của vấn đề trong khi kiểm tra các phần khác, để tiết kiệm cho mình khi phải viết 10 tỷ bài kiểm tra cho 10 tỷ hoán vị.

Chỉnh sửa: Tôi muốn thêm một ví dụ, nhưng không có thời gian sớm hơn.

Hãy xem xét một thuật toán sắp xếp tại chỗ. Chúng ta có thể tiếp tục và viết các bài kiểm tra bao gồm đầu cuối của mảng, đầu dưới của mảng và tất cả các loại kết hợp kỳ lạ ở giữa. Đối với mỗi người, chúng ta sẽ phải xây dựng một mảng hoàn chỉnh của một số loại đối tượng. Điều này sẽ mất thời gian.

Hoặc chúng ta có thể giải quyết vấn đề trong bốn phần:

  1. Đi qua mảng.
  2. So sánh các mục đã chọn.
  3. Chuyển đổi các mặt hàng.
  4. Phối hợp ba điều trên.

Đầu tiên là phần phức tạp duy nhất của vấn đề nhưng bằng cách trừu tượng hóa nó khỏi phần còn lại, bạn đã làm cho nó trở nên đơn giản hơn nhiều.

Thứ hai gần như chắc chắn được xử lý bởi chính đối tượng, ít nhất là tùy chọn, trong nhiều khung công tác tĩnh sẽ có một giao diện để hiển thị liệu chức năng đó có được thực hiện hay không. Vì vậy, bạn không cần phải kiểm tra điều này.

Thứ ba là cực kỳ dễ dàng để kiểm tra.

Thứ tư chỉ xử lý hai con trỏ, yêu cầu lớp truyền tải di chuyển các con trỏ xung quanh, gọi một phép so sánh và dựa trên kết quả của phép so sánh đó, gọi các mục được hoán đổi. Nếu bạn đã giả mạo ba vấn đề đầu tiên, bạn có thể kiểm tra vấn đề này rất dễ dàng.

Làm thế nào chúng ta đã dẫn đến một thiết kế tốt hơn ở đây? Hãy nói rằng bạn đã giữ nó đơn giản và thực hiện một loại bong bóng. Nó hoạt động nhưng, khi bạn đi vào sản xuất và nó phải xử lý một triệu đối tượng, nó quá chậm. Tất cả những gì bạn phải làm là viết chức năng truyền tải mới và trao đổi nó. Bạn không phải đối phó với sự phức tạp khi xử lý ba vấn đề khác.

Điều này, bạn sẽ tìm thấy, là sự khác biệt giữa thử nghiệm đơn vị và TDD. Người kiểm tra đơn vị sẽ nói rằng điều này đã làm cho các thử nghiệm của bạn trở nên dễ vỡ, rằng nếu bạn đã thử nghiệm đầu vào và đầu ra đơn giản thì bây giờ bạn sẽ không phải viết thêm các thử nghiệm cho chức năng mới của mình. TDDer sẽ nói rằng tôi đã phân tách các mối quan tâm một cách phù hợp để mỗi lớp tôi có làm một việc và một việc tốt.


1

Không thể kiểm tra mọi hoán vị của một tính toán với nhiều biến. Nhưng điều đó không có gì mới, nó luôn đúng với bất kỳ chương trình nào trên mức độ phức tạp của đồ chơi. Điểm của các bài kiểm tra là để xác minh tính chất của tính toán. Ví dụ, việc sắp xếp một danh sách với 1000 số cần một số nỗ lực, nhưng bất kỳ giải pháp riêng lẻ nào cũng có thể được xác minh rất dễ dàng. Bây giờ, mặc dù có 1000! có thể (các lớp) đầu vào cho chương trình đó và bạn không thể kiểm tra tất cả, hoàn toàn đủ để tạo ngẫu nhiên 1000 đầu vào và xác minh rằng đầu ra thực sự được sắp xếp. Tại sao? Bởi vì gần như không thể viết một chương trình sắp xếp đáng tin cậy 1000 vectơ được tạo ngẫu nhiên mà không nói chung cũng đúng (trừ khi bạn cố tình gian lận nó để thao túng một số phép thuật đầu vào ...)

Bây giờ, nói chung mọi thứ phức tạp hơn một chút. Thực sự lỗi khi một người gửi thư sẽ không gửi email cho người dùng nếu họ có 'f' trong tên người dùng của họ và ngày trong tuần là thứ Sáu. Nhưng tôi coi đó là lãng phí nỗ lực cố gắng để dự đoán sự kỳ lạ như vậy. Bộ kiểm tra của bạn sẽ cung cấp cho bạn một sự tự tin ổn định rằng hệ thống thực hiện những gì bạn mong đợi về các đầu vào mà bạn mong đợi. Nếu nó thực hiện những điều thú vị trong một số trường hợp thú vị nhất định, bạn sẽ sớm nhận thấy ngay sau khi bạn thử trường hợp thú vị đầu tiên, và sau đó bạn có thể viết một bài kiểm tra cụ thể đối với trường hợp đó (thường sẽ bao gồm toàn bộ một lớp các trường hợp tương tự).


Nếu bạn tạo ngẫu nhiên 1000 đầu vào, làm thế nào để bạn kiểm tra đầu ra? Chắc chắn thử nghiệm như vậy sẽ liên quan đến một số logic, mà bản thân nó không được thử nghiệm. Vậy bạn có kiểm tra bài thi không? Làm sao? Điểm là, bạn nên kiểm tra logic bằng cách chuyển trạng thái - đầu vào X đã cho, đầu ra phải là Y. Một thử nghiệm liên quan đến logic dễ bị lỗi nhiều như logic mà nó kiểm tra. Theo thuật ngữ logic, việc biện minh cho một đối số bằng một đối số khác đưa bạn vào con đường hồi quy hoài nghi - bạn phải đưa ra một số khẳng định. Những khẳng định này là bài kiểm tra của bạn.
Izhaki

0

Lấy các trường hợp cạnh cộng với một số đầu vào ngẫu nhiên.

Lấy ví dụ sắp xếp:

  • Sắp xếp một vài danh sách ngẫu nhiên
  • Lấy một danh sách đã được sắp xếp
  • Lấy một danh sách theo thứ tự ngược lại
  • Lấy một danh sách gần như đã được sắp xếp

Nếu nó hoạt động nhanh đối với những thứ này, bạn có thể khá chắc chắn rằng nó sẽ hoạt động cho tất cả các đầu vào.

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.