Đâu là ranh giới giữa logic ứng dụng thử nghiệm đơn vị và các cấu trúc ngôn ngữ không tin cậy?


87

Hãy xem xét một chức năng như thế này:

function savePeople(dataStore, people) {
    people.forEach(person => dataStore.savePerson(person));
}

Nó có thể được sử dụng như thế này:

myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);

Chúng ta hãy giả sử rằng Storecó các bài kiểm tra đơn vị riêng của mình, hoặc do nhà cung cấp cung cấp. Trong mọi trường hợp, chúng tôi tin tưởng Store. Và chúng ta hãy giả định thêm rằng việc xử lý lỗi - ví dụ: lỗi ngắt kết nối cơ sở dữ liệu - không phải là trách nhiệm của savePeople. Thật vậy, chúng ta hãy giả sử rằng chính cửa hàng là cơ sở dữ liệu ma thuật không thể có lỗi. Với những giả định này, câu hỏi là:

Nên savePeople()được kiểm tra đơn vị, hoặc các kiểm tra như vậy có đủ để kiểm tra forEachcấu trúc ngôn ngữ tích hợp không?

Tất nhiên chúng ta có thể vượt qua trong một bản giả dataStorevà khẳng định rằng nó dataStore.savePerson()được gọi một lần cho mỗi người. Bạn chắc chắn có thể đưa ra lập luận rằng một thử nghiệm như vậy cung cấp bảo mật chống lại các thay đổi triển khai: ví dụ: nếu chúng tôi quyết định thay thế forEachbằng một forvòng lặp truyền thống hoặc một số phương pháp lặp khác. Vì vậy, bài kiểm tra không hoàn toàn tầm thường. Nhưng nó dường như rất gần ...


Đây là một ví dụ khác có thể hiệu quả hơn. Hãy xem xét một chức năng không làm gì ngoài việc phối hợp các đối tượng hoặc chức năng khác. Ví dụ:

function bakeCookies(dough, pan, oven) {
    panWithRawCookies = pan.add(dough);
    oven.addPan(panWithRawCookies);
    oven.bakeCookies();
    oven.removePan();
}

Làm thế nào một chức năng như thế này nên được kiểm tra đơn vị, giả sử bạn nghĩ rằng nó nên? Thật khó cho tôi để tưởng tượng bất kỳ loại bài kiểm tra đơn vị đó không chỉ đơn giản là nhạo báng dough, panoven, và sau đó khẳng định rằng phương pháp này được gọi là trên chúng. Nhưng một thử nghiệm như vậy không làm gì khác hơn là sao chép việc thực hiện chính xác của chức năng.

Có phải điều này không có khả năng kiểm tra chức năng theo cách hộp đen có ý nghĩa cho thấy một lỗ hổng thiết kế với chính chức năng đó? Nếu vậy, làm thế nào nó có thể được cải thiện?


Để làm rõ hơn nữa cho câu hỏi thúc đẩy bakeCookiesví dụ, tôi sẽ thêm một kịch bản thực tế hơn, đó là một kịch bản mà tôi đã gặp phải khi cố gắng thêm các bài kiểm tra và mã hóa lại mã kế thừa.

Khi người dùng tạo tài khoản mới, một số điều cần phải xảy ra: 1) hồ sơ người dùng mới cần được tạo trong cơ sở dữ liệu 2) một email chào mừng cần được gửi 3) địa chỉ IP của người dùng cần được ghi lại để gian lận mục đích.

Vì vậy, chúng tôi muốn tạo một phương thức liên kết tất cả các bước "người dùng mới":

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.insertUserRecord(validateduserData);
  emailService.sendWelcomeEmail(validatedUserData);
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

Lưu ý rằng nếu bất kỳ phương pháp nào trong số các phương thức này gây ra lỗi, chúng tôi muốn lỗi này nổi lên theo mã cuộc gọi, để nó có thể xử lý lỗi khi nó thấy phù hợp. Nếu nó được gọi bởi mã API, nó có thể dịch lỗi thành mã phản hồi http thích hợp. Nếu nó được gọi bởi một giao diện web, nó có thể dịch lỗi thành một thông báo phù hợp sẽ được hiển thị cho người dùng, v.v. Vấn đề là chức năng này không biết cách xử lý các lỗi có thể bị ném.

Bản chất của sự nhầm lẫn của tôi là để kiểm tra đơn vị một chức năng như vậy, có vẻ như cần phải lặp lại việc thực hiện chính xác trong chính thử nghiệm (bằng cách chỉ định rằng các phương thức được gọi trên các giả theo một thứ tự nhất định) và điều đó có vẻ sai.


44
Sau khi nó chạy. Bạn có bánh quy không
Ewan

6
liên quan đến cập nhật của bạn: tại sao bạn muốn chế giễu một cái chảo? hay bột? những âm thanh này giống như các đối tượng trong bộ nhớ đơn giản nên tạo ra tầm thường, và vì vậy không có lý do gì bạn không nên kiểm tra chúng như một đơn vị. hãy nhớ rằng, "đơn vị" trong "kiểm thử đơn vị" không có nghĩa là "một lớp duy nhất". nó có nghĩa là "đơn vị mã nhỏ nhất có thể được sử dụng để hoàn thành công việc". một cái chảo có lẽ không có gì khác hơn là một vật chứa cho các vật thể bột, vì vậy nó được thử nghiệm cách ly nó thay vì chỉ lái thử phương pháp bakeCookies từ bên ngoài.
sara

11
Vào cuối ngày, nguyên tắc cơ bản trong công việc ở đây là bạn viết đủ các bài kiểm tra để đảm bảo với bản thân rằng mã hoạt động và đó là một "hoàng yến trong mỏ than" đầy đủ khi ai đó thay đổi điều gì đó. Đó là nó. Không có câu thần chú, giả định công thức hoặc xác nhận giáo điều, đó là lý do tại sao độ bao phủ mã 85% đến 90% (không phải 100%) được coi là xuất sắc.
Robert Harvey

5
@RobertHarvey không may là các công thức và các âm thanh TDD cắn, trong khi chắc chắn sẽ giúp bạn có được những cái gật đầu nhiệt tình, không giúp giải quyết các vấn đề trong thế giới thực. cho rằng bạn cần phải làm bẩn tay và mạo hiểm trả lời một câu hỏi thực tế
Jonah

4
Kiểm tra đơn vị theo thứ tự độ phức tạp giảm dần. Tin tôi đi, bạn sẽ hết thời gian trước khi bạn có được chức năng này
Neil McGuigan

Câu trả lời:


118

Có nên savePeople()thử nghiệm đơn vị? Đúng. Bạn không kiểm tra dataStore.savePersonhoạt động hoặc kết nối db hoạt động hay thậm chí là foreachhoạt động. Bạn đang thử nghiệm savePeoplethực hiện lời hứa mà nó đưa ra thông qua hợp đồng của nó.

Hãy tưởng tượng kịch bản này: ai đó thực hiện một bộ tái cấu trúc lớn của cơ sở mã và vô tình xóa forEachphần thực hiện để nó luôn chỉ lưu mục đầu tiên. Bạn có muốn thử nghiệm đơn vị để nắm bắt điều đó không?


20
@RobertHarvey: Có rất nhiều vùng màu xám và việc phân biệt, IMO, không quan trọng. Mặc dù vậy, bạn đã đúng - không thực sự quan trọng để kiểm tra rằng "nó gọi đúng chức năng" mà là "nó làm đúng" bất kể nó hoạt động như thế nào. Điều quan trọng là kiểm tra điều đó, với một bộ đầu vào cụ thể cho chức năng bạn có được một bộ đầu ra cụ thể. Tuy nhiên, tôi có thể thấy làm thế nào câu cuối cùng có thể gây nhầm lẫn, vì vậy tôi đã loại bỏ nó.
Bryan Oakley

64
"Bạn đang thử nghiệm rằng SaveP People thực hiện đúng lời hứa mà họ đưa ra thông qua hợp đồng của mình." Điều này. Rất nhiều điều này.
Lovis

2
Trừ khi bạn có một bài kiểm tra hệ thống "kết thúc để kết thúc" bao gồm nó.
Ian

6
@Ian Kết thúc kiểm tra kết thúc không thay thế kiểm tra đơn vị, chúng là miễn phí. Chỉ vì bạn có thể có kết thúc kiểm tra kết thúc để đảm bảo bạn lưu một danh sách những người không có nghĩa là bạn không nên có một bài kiểm tra đơn vị để bao quát nó.
Vincent Savard

4
@VincentSavard, nhưng chi phí / lợi ích của một bài kiểm tra đơn vị sẽ giảm nếu rủi ro được kiểm soát theo cách bao phấn.
Ian

36

Thông thường loại câu hỏi này xuất hiện khi mọi người thực hiện phát triển "thử nghiệm sau". Tiếp cận vấn đề này theo quan điểm của TDD, nơi các bài kiểm tra đến trước khi thực hiện và tự hỏi lại câu hỏi này như một bài tập.

Ít nhất là trong ứng dụng TDD của tôi, thường là bên ngoài, tôi sẽ không thực hiện một chức năng như savePeoplesau khi đã triển khai savePerson. Các chức năng savePeoplesavePersonsẽ bắt đầu như một và được điều khiển thử nghiệm từ cùng một bài kiểm tra đơn vị; sự tách biệt giữa hai người sẽ phát sinh sau một vài thử nghiệm, trong bước tái cấu trúc. Chế độ làm việc này cũng sẽ đặt ra câu hỏi về chức năng savePeoplenên ở đâu - đó là chức năng miễn phí hay là một phần của dataStore.

Cuối cùng, các cuộc thử nghiệm sẽ không chỉ kiểm tra xem bạn có thể tiết kiệm một cách chính xác một Persontrong Store, nhưng cũng có nhiều người. Điều này cũng sẽ khiến tôi đặt câu hỏi nếu các kiểm tra khác là cần thiết, chẳng hạn, "Tôi có cần đảm bảo rằng savePeoplechức năng đó là nguyên tử, có thể lưu tất cả hay không?", "Nó có thể trả lại lỗi cho những người không thể ' T được lưu? Làm thế nào những lỗi đó sẽ trông như thế nào? ", và như vậy. Tất cả số tiền này nhiều hơn nhiều so với việc chỉ kiểm tra việc sử dụng một forEachhoặc các hình thức lặp khác.

Mặc dù, nếu yêu cầu lưu nhiều hơn một người chỉ sau khi savePersonđã được gửi, thì tôi sẽ cập nhật các thử nghiệm hiện có savePersonđể chạy qua chức năng mới savePeople, đảm bảo rằng nó vẫn có thể cứu một người bằng cách chỉ cần ủy thác lúc đầu, sau đó kiểm tra hành vi cho nhiều người thông qua các thử nghiệm mới, suy nghĩ xem có cần thiết phải thực hiện hành vi nguyên tử hay không.


4
Về cơ bản kiểm tra giao diện, không phải việc thực hiện.
Snoop

8
Điểm công bằng và sâu sắc. Tuy nhiên, tôi cảm thấy bằng cách nào đó câu hỏi thực sự của tôi đang bị né tránh :) Câu trả lời của bạn là: "Trong thế giới thực, trong một hệ thống được thiết kế tốt, tôi không nghĩ rằng phiên bản đơn giản hóa của vấn đề của bạn sẽ tồn tại." Một lần nữa, công bằng, nhưng tôi đặc biệt tạo ra phiên bản đơn giản hóa này để làm nổi bật bản chất của một vấn đề tổng quát hơn. Nếu bạn không thể vượt qua bản chất nhân tạo của ví dụ, có lẽ bạn có thể tưởng tượng một ví dụ khác trong đó bạn có lý do chính đáng cho một chức năng tương tự, chỉ thực hiện lặp lại và ủy quyền. Hoặc có lẽ bạn nghĩ rằng nó chỉ đơn giản là không thể?
Giô-na

@Jonah cập nhật. Tôi hy vọng nó trả lời câu hỏi của bạn tốt hơn một chút. Đây là tất cả dựa trên ý kiến ​​và có thể chống lại mục tiêu cho trang web này, nhưng nó chắc chắn là một cuộc thảo luận rất thú vị để có. Nhân tiện, tôi đã cố gắng trả lời từ quan điểm của công việc chuyên môn, nơi chúng tôi phải cố gắng để kiểm tra đơn vị cho tất cả các hành vi ứng dụng, bất kể việc triển khai có thể tầm thường như thế nào, bởi vì chúng tôi có nhiệm vụ xây dựng một thử nghiệm tốt và hệ thống tài liệu cho người bảo trì mới nếu chúng tôi rời đi. Đối với các dự án cá nhân hoặc, không quan trọng (tiền cũng rất quan trọng), tôi có ý kiến ​​rất khác.
MichelHenrich

Cảm ơn các cập nhật. Làm thế nào chính xác bạn sẽ kiểm tra savePeople? Như tôi đã mô tả trong đoạn cuối của OP hoặc một số cách khác?
Giô-na

1
Xin lỗi, tôi đã không làm rõ bản thân mình với phần "không có giả liên quan". Tôi có nghĩa là tôi sẽ không sử dụng một bản giả cho savePersonchức năng như bạn đề xuất, thay vào đó tôi sẽ kiểm tra nó thông qua tổng quát hơn savePeople. Các bài kiểm tra đơn vị Storesẽ được thay đổi để chạy qua savePeoplechứ không phải gọi trực tiếp savePerson, vì vậy đối với điều này, không có giả định nào được sử dụng. Nhưng tất nhiên cơ sở dữ liệu không nên có mặt, vì chúng tôi muốn cách ly các vấn đề mã hóa khỏi các vấn đề tích hợp khác nhau xảy ra với cơ sở dữ liệu thực tế, vì vậy ở đây chúng tôi vẫn có một bản giả.
MichelHenrich

21

Nên lưu đơn vị ()

Vâng, nó nên. Nhưng hãy cố gắng viết các điều kiện kiểm tra của bạn theo cách độc lập với việc thực hiện. Ví dụ: biến ví dụ sử dụng của bạn thành một bài kiểm tra đơn vị:

function testSavePeople() {
    myDataStore = new Store('some connection string', 'password');
    myPeople = ['Joe', 'Maggie', 'John'];
    savePeople(myDataStore, myPeople);
    assert(myDataStore.containsPerson('Joe'));
    assert(myDataStore.containsPerson('Maggie'));
    assert(myDataStore.containsPerson('John'));
}

Bài kiểm tra này thực hiện nhiều việc:

  • nó xác minh hợp đồng của chức năng savePeople()
  • nó không quan tâm đến việc thực hiện savePeople()
  • nó ghi lại cách sử dụng ví dụ của savePeople()

Hãy lưu ý rằng bạn vẫn có thể giả định / sơ khai / giả mạo lưu trữ dữ liệu. Trong trường hợp này tôi sẽ không kiểm tra các cuộc gọi chức năng rõ ràng, nhưng cho kết quả của hoạt động. Bằng cách này, bài kiểm tra của tôi được chuẩn bị cho những thay đổi / nhà tái cấu trúc trong tương lai.

Ví dụ: việc triển khai cửa hàng dữ liệu của bạn có thể cung cấp một saveBulkPerson()phương thức trong tương lai - bây giờ thay đổi đối với việc triển khai savePeople()sử dụng saveBulkPerson()sẽ không phá vỡ thử nghiệm đơn vị miễn là saveBulkPerson()hoạt động như mong đợi. Và nếu saveBulkPerson()bằng cách nào đó không hoạt động như mong đợi, bài kiểm tra đơn vị của bạn sẽ nắm bắt được điều đó.

hoặc các bài kiểm tra như vậy có đủ để kiểm tra cấu trúc ngôn ngữ forEach tích hợp không?

Như đã nói, hãy thử kiểm tra các kết quả mong đợi và giao diện chức năng, không phải để triển khai (trừ khi bạn đang thực hiện kiểm tra tích hợp - sau đó có thể sử dụng các lệnh gọi chức năng cụ thể). Nếu có nhiều cách để thực hiện một chức năng, tất cả chúng sẽ hoạt động với bài kiểm tra đơn vị của bạn.

Về cập nhật câu hỏi của bạn:

Kiểm tra thay đổi trạng thái! Ví dụ, một số bột sẽ được sử dụng. Theo triển khai của bạn, khẳng định rằng lượng sử dụng doughphù hợp panhoặc khẳng định rằng đã doughsử dụng hết. Khẳng định rằng pancó chứa cookie sau khi gọi hàm. Khẳng định rằng oventrống / ở trạng thái như trước.

Đối với các xét nghiệm bổ sung, xác minh trường hợp cạnh: Điều gì sẽ xảy ra nếu ovenkhông có sản phẩm nào trước khi cuộc gọi? Điều gì xảy ra nếu không đủ dough? Nếu panđã đầy?

Bạn sẽ có thể suy ra tất cả các dữ liệu cần thiết cho các thử nghiệm này từ chính các đối tượng bột, chảo và lò nướng. Không cần phải nắm bắt các cuộc gọi chức năng. Đối xử với các chức năng như thể thực hiện nó sẽ không có sẵn cho bạn!

Trên thực tế, hầu hết người dùng TDD viết bài kiểm tra của họ trước khi họ viết hàm để họ không phụ thuộc vào việc triển khai thực tế.


Đối với bổ sung mới nhất của bạn:

Khi người dùng tạo tài khoản mới, một số điều cần phải xảy ra: 1) hồ sơ người dùng mới cần được tạo trong cơ sở dữ liệu 2) một email chào mừng cần được gửi 3) địa chỉ IP của người dùng cần được ghi lại để gian lận mục đích.

Vì vậy, chúng tôi muốn tạo một phương thức liên kết tất cả các bước "người dùng mới":

function createNewUser(validatedUserData, emailService, dataStore) {
    userId = dataStore.insertUserRecord(validateduserData);
    emailService.sendWelcomeEmail(validatedUserData);
    dataStore.recordIpAddress(userId, validatedUserData.ip);
}

Đối với một chức năng như thế này, tôi sẽ giả / stub / fake (bất cứ điều gì có vẻ tổng quát hơn) dataStoreemailServicecác tham số. Hàm này không tự thực hiện bất kỳ chuyển đổi trạng thái nào trên bất kỳ tham số nào, nó ủy thác chúng cho các phương thức của một số trong số chúng. Tôi sẽ cố gắng xác minh rằng lệnh gọi hàm đã thực hiện 4 điều:

  • nó chèn một người dùng vào kho lưu trữ dữ liệu
  • nó đã gửi (hoặc ít nhất được gọi là phương thức tương ứng) một email chào mừng
  • nó đã ghi lại IP của người dùng vào kho lưu trữ dữ liệu
  • nó ủy thác bất kỳ ngoại lệ / lỗi nào nó gặp phải (nếu có)

3 kiểm tra đầu tiên có thể được thực hiện với giả, sơ khai hoặc giả dataStoreemailService(bạn thực sự không muốn gửi email khi kiểm tra). Vì tôi phải tìm kiếm một số ý kiến, đây là những điểm khác biệt:

  • Giả là một đối tượng hành xử giống như ban đầu và ở một mức độ nhất định không thể phân biệt. Mã của nó thường có thể được sử dụng lại qua các bài kiểm tra. Ví dụ, đây có thể là một cơ sở dữ liệu trong bộ nhớ đơn giản cho trình bao bọc cơ sở dữ liệu.
  • Một sơ khai chỉ thực hiện càng nhiều càng cần thiết để thực hiện các hoạt động cần thiết của thử nghiệm này. Trong hầu hết các trường hợp, sơ khai chỉ dành riêng cho một bài kiểm tra hoặc một nhóm các bài kiểm tra chỉ cần một bộ nhỏ các phương pháp của bản gốc. Trong ví dụ này, nó có thể là một dataStorephiên bản phù hợp insertUserRecord()recordIpAddress().
  • Giả là một đối tượng cho phép bạn xác minh cách sử dụng nó (thường xuyên nhất bằng cách cho phép bạn đánh giá các cuộc gọi đến các phương thức của nó). Tôi sẽ cố gắng sử dụng chúng một cách tiết kiệm trong các bài kiểm tra đơn vị vì bằng cách sử dụng chúng, bạn thực sự cố gắng kiểm tra việc thực hiện chức năng và không tuân thủ giao diện của nó, nhưng chúng vẫn có những công dụng của chúng. Nhiều khung mô phỏng tồn tại để giúp bạn tạo ra chỉ giả mà bạn cần.

Lưu ý rằng nếu bất kỳ phương pháp nào trong số các phương thức này gây ra lỗi, chúng tôi muốn lỗi này nổi lên theo mã cuộc gọi, để nó có thể xử lý lỗi khi nó thấy phù hợp. Nếu nó được gọi bởi mã API, nó có thể dịch lỗi thành mã phản hồi HTTP thích hợp. Nếu nó được gọi bởi một giao diện web, nó có thể dịch lỗi thành một thông báo phù hợp sẽ được hiển thị cho người dùng, v.v. Vấn đề là chức năng này không biết cách xử lý các lỗi có thể bị ném.

Các trường hợp ngoại lệ / lỗi dự kiến ​​là các trường hợp kiểm tra hợp lệ: Bạn xác nhận rằng, trong trường hợp sự kiện như vậy xảy ra, hàm sẽ hoạt động theo cách bạn mong đợi. Điều này có thể đạt được bằng cách cho phép đối tượng giả / giả / gốc tương ứng ném khi muốn.

Bản chất của sự nhầm lẫn của tôi là để kiểm tra đơn vị một chức năng như vậy, có vẻ như cần phải lặp lại việc thực hiện chính xác trong chính thử nghiệm (bằng cách chỉ định rằng các phương thức được gọi trên các giả theo một thứ tự nhất định) và điều đó có vẻ sai.

Đôi khi điều này phải được thực hiện (mặc dù bạn chủ yếu quan tâm đến điều này trong các bài kiểm tra tích hợp). Thường xuyên hơn, có nhiều cách khác để xác minh các tác dụng phụ / thay đổi trạng thái dự kiến.

Xác minh các chức năng gọi chính xác làm cho các bài kiểm tra đơn vị khá dễ vỡ: Chỉ những thay đổi nhỏ đối với chức năng ban đầu khiến chúng bị lỗi. Điều này có thể được mong muốn hoặc không, nhưng nó yêu cầu thay đổi (các) thử nghiệm đơn vị tương ứng bất cứ khi nào bạn thay đổi một chức năng (có thể tái cấu trúc, tối ưu hóa, sửa lỗi, ...).

Đáng buồn thay, trong trường hợp đó, bài kiểm tra đơn vị mất một số độ tin cậy: vì nó đã được thay đổi, nó không xác nhận chức năng sau khi thay đổi hoạt động giống như trước đây.

Ví dụ: xem xét ai đó thêm một cuộc gọi đến oven.preheat()(tối ưu hóa!) Trong ví dụ nướng bánh quy của bạn:

  • Nếu bạn chế nhạo đối tượng lò nướng, nó sẽ không mong đợi cuộc gọi đó và thất bại trong thử nghiệm, mặc dù hành vi có thể quan sát được của phương pháp không thay đổi (hy vọng bạn vẫn có một chảo bánh quy).
  • Một sơ khai có thể hoặc có thể không thất bại, tùy thuộc vào việc bạn chỉ thêm các phương thức được kiểm tra hay toàn bộ giao diện với một số phương thức giả.
  • Giả mạo không nên thất bại, vì nó nên thực hiện phương thức (theo giao diện)

Trong các thử nghiệm đơn vị của tôi, tôi cố gắng chung chung nhất có thể: Nếu việc triển khai thay đổi, nhưng hành vi có thể nhìn thấy (từ góc nhìn của người gọi) vẫn giống nhau, các thử nghiệm của tôi sẽ vượt qua. Lý tưởng nhất, trường hợp duy nhất tôi cần thay đổi một bài kiểm tra đơn vị hiện tại phải là một sửa lỗi (của bài kiểm tra, không phải là chức năng được kiểm tra).


1
Vấn đề là ngay khi bạn viết, myDataStore.containsPerson('Joe')bạn đang giả sử sự tồn tại của cơ sở dữ liệu kiểm tra chức năng. Khi bạn làm điều đó, bạn đang viết một bài kiểm tra tích hợp chứ không phải bài kiểm tra đơn vị.
Giô-na

Tôi giả sử rằng tôi có thể dựa vào việc có một kho lưu trữ dữ liệu thử nghiệm (tôi không quan tâm đó là thật hay giả) và mọi thứ đều hoạt động như đã thiết lập (vì tôi đã có các thử nghiệm đơn vị cho những trường hợp đó rồi). Điều duy nhất mà bài kiểm tra muốn kiểm tra là savePeople()thực sự thêm những người đó vào bất kỳ kho lưu trữ dữ liệu nào bạn cung cấp miễn là kho dữ liệu đó thực hiện giao diện dự kiến. Một thử nghiệm tích hợp sẽ là, ví dụ, kiểm tra xem trình bao bọc cơ sở dữ liệu của tôi thực sự thực hiện đúng các cuộc gọi cơ sở dữ liệu cho một cuộc gọi phương thức.
hoffmale

Để làm rõ, nếu bạn đang sử dụng một giả, tất cả những gì bạn có thể làm là khẳng định rằng một phương thức trên giả đó đã được gọi , có lẽ với một số tham số cụ thể. Bạn không thể khẳng định trạng thái của giả sau đó. Vì vậy, nếu bạn muốn thực hiện các xác nhận về trạng thái của cơ sở dữ liệu sau khi gọi phương thức đang được thử nghiệm, như trong myDataStore.containsPerson('Joe'), bạn phải sử dụng một loại db chức năng nào đó. Khi bạn thực hiện bước đó, nó không còn là bài kiểm tra đơn vị nữa.
Giô-na

1
nó không phải là một cơ sở dữ liệu thực - chỉ là một đối tượng thực hiện giao diện giống như kho lưu trữ dữ liệu thực (đọc: nó vượt qua các bài kiểm tra đơn vị có liên quan cho giao diện lưu trữ dữ liệu). Tôi vẫn coi đây là một sự giả tạo. Hãy để mock lưu trữ mọi thứ được thêm bằng bất kỳ phương thức nào để làm như vậy trong một mảng và kiểm tra xem dữ liệu thử nghiệm (các phần tử của myPeople) có trong mảng không. IMHO một giả vẫn phải có hành vi có thể quan sát được như mong đợi từ một đối tượng thực, nếu không, bạn đang kiểm tra việc tuân thủ giao diện giả, không phải giao diện thực.
hoffmale

"Hãy để giả lập lưu trữ mọi thứ được thêm bằng bất kỳ phương thức nào để thực hiện điều đó trong một mảng và kiểm tra xem dữ liệu kiểm tra (các phần tử của myP People) có trong mảng không" - đó vẫn là cơ sở dữ liệu "thực", chỉ là một quảng cáo, trong bộ nhớ bạn đã xây dựng. "IMHO một bản giả vẫn nên có hành vi có thể quan sát được như mong đợi từ một vật thật" - Tôi cho rằng bạn có thể biện hộ cho điều đó, nhưng đó không phải là "giả" có nghĩa là trong tài liệu thử nghiệm hoặc trong bất kỳ thư viện phổ biến nào tôi ' đã từng thấy. Một giả chỉ đơn giản xác minh rằng các phương thức dự kiến ​​được gọi với các tham số dự kiến.
Giô-na

13

Giá trị chính mà một bài kiểm tra cung cấp là nó làm cho việc thực hiện của bạn có thể tái cấu trúc.

Tôi đã từng thực hiện rất nhiều tối ưu hóa hiệu suất trong sự nghiệp của mình và thường gặp vấn đề với mẫu chính xác mà bạn đã thể hiện: để lưu N thực thể vào cơ sở dữ liệu, thực hiện N chèn. Nó thường hiệu quả hơn khi thực hiện chèn số lượng lớn bằng cách sử dụng một câu lệnh.

Mặt khác, chúng tôi cũng không muốn tối ưu hóa sớm. Nếu bạn thường chỉ cứu 1 - 3 người một lần, thì việc viết một lô được tối ưu hóa có thể là quá mức cần thiết.

Với một bài kiểm tra đơn vị phù hợp, bạn có thể viết nó theo cách bạn đã thực hiện ở trên và nếu bạn thấy bạn cần tối ưu hóa nó, bạn có thể tự do làm điều đó với mạng lưới an toàn của bài kiểm tra tự động để bắt lỗi. Đương nhiên, điều này thay đổi dựa trên chất lượng của các bài kiểm tra, vì vậy hãy kiểm tra tự do và kiểm tra tốt.

Lợi thế thứ cấp để kiểm tra đơn vị hành vi này là dùng làm tài liệu cho mục đích của nó. Ví dụ tầm thường này có thể rõ ràng, nhưng đưa ra điểm tiếp theo bên dưới, nó có thể rất quan trọng.

Ưu điểm thứ ba, mà những người khác đã chỉ ra, là bạn có thể kiểm tra các chi tiết dưới vỏ bọc rất khó kiểm tra bằng các thử nghiệm tích hợp hoặc chấp nhận. Ví dụ: nếu có một yêu cầu rằng tất cả người dùng phải được lưu nguyên tử, thì bạn có thể viết một trường hợp thử nghiệm cho điều đó, cho bạn một cách để biết nó hoạt động như mong đợi và cũng là tài liệu cho một yêu cầu có thể không rõ ràng cho các nhà phát triển mới.

Tôi sẽ thêm một ý nghĩ mà tôi đã nhận được từ một người hướng dẫn TDD. Đừng thử phương pháp. Kiểm tra hành vi. Nói cách khác, bạn không kiểm tra savePeoplehoạt động, bạn đang kiểm tra rằng nhiều người dùng có thể được lưu trong một cuộc gọi.

Tôi thấy khả năng của mình để kiểm tra đơn vị chất lượng và TDD cải thiện khi tôi ngừng suy nghĩ về kiểm thử đơn vị khi xác minh rằng chương trình hoạt động, nhưng thay vào đó, họ xác minh rằng một đơn vị mã thực hiện những gì tôi mong đợi . Đó là những khác nhau. Họ không xác minh nó hoạt động, nhưng họ xác minh nó làm những gì tôi nghĩ nó làm. Khi tôi bắt đầu nghĩ theo cách đó, quan điểm của tôi đã thay đổi.


Ví dụ tái cấu trúc chèn số lượng lớn là một ví dụ tốt. Thử nghiệm đơn vị khả thi mà tôi đã đề xuất trong OP - tuy nhiên, một giả lập dataStore đã savePersongọi nó cho từng người trong danh sách - tuy nhiên sẽ phá vỡ với một phép tái cấu trúc chèn số lượng lớn. Mà theo tôi chỉ ra rằng đó là một bài kiểm tra đơn vị kém. Tuy nhiên, tôi không thấy một giải pháp thay thế nào có thể vượt qua cả triển khai hàng loạt và một người tiết kiệm cho mỗi người, mà không sử dụng cơ sở dữ liệu thử nghiệm thực tế và khẳng định chống lại điều đó, điều này có vẻ sai. Bạn có thể cung cấp một bài kiểm tra hoạt động cho cả hai triển khai?
Giô-na

1
@ jpmc26 Điều gì về một bài kiểm tra mà mọi người đã được cứu ...?
Immibis

1
@immibis Tôi không hiểu điều đó có nghĩa là gì. Có lẽ, cửa hàng thực sự được hỗ trợ bởi cơ sở dữ liệu, vì vậy bạn phải giả định hoặc khai thác nó để kiểm tra đơn vị. Vì vậy, tại thời điểm đó, bạn sẽ kiểm tra xem giả hoặc gốc của bạn có thể lưu trữ các đối tượng. Điều đó hoàn toàn vô dụng. Điều tốt nhất bạn có thể làm là khẳng định rằng savePersonphương thức được gọi cho mỗi đầu vào và nếu bạn thay thế vòng lặp bằng một số lượng lớn, bạn sẽ không gọi phương thức đó nữa. Vì vậy, bài kiểm tra của bạn sẽ phá vỡ. Nếu bạn có một cái gì đó khác trong tâm trí, tôi mở cho nó, nhưng tôi chưa thấy nó. (Và không thấy đó là quan điểm của tôi.)
jpmc26

1
@immibis Tôi không coi đó là một bài kiểm tra hữu ích. Sử dụng kho dữ liệu giả không mang lại cho tôi sự tự tin, nó sẽ hoạt động với hàng thật. Làm thế nào để tôi biết tác phẩm giả của tôi giống như thật? Tôi chỉ muốn để một bộ các bài kiểm tra tích hợp bao gồm nó. (Tôi có lẽ nên làm rõ rằng tôi có nghĩa là "bất kỳ bài kiểm tra đơn vị nào " trong bình luận đầu tiên của tôi ở đây.)
jpmc26

1
@immibis Tôi thực sự đang xem xét lại vị trí của mình. Tôi đã nghi ngờ về giá trị của các bài kiểm tra đơn vị vì các vấn đề như thế này, nhưng có lẽ tôi đang đánh giá thấp giá trị ngay cả khi bạn giả mạo đầu vào. Tôi làm biết rằng thử nghiệm đầu vào / đầu ra có xu hướng được nhiều ích hơn các bài kiểm tra nặng giả, nhưng có lẽ từ chối để thay thế một đầu vào với một giả thực sự là một phần của vấn đề ở đây.
jpmc26

6

Có nên bakeCookies()thử nghiệm? Đúng.

Làm thế nào một chức năng như thế này nên được kiểm tra đơn vị, giả sử bạn nghĩ rằng nó nên? Thật khó cho tôi để tưởng tượng bất kỳ loại thử nghiệm đơn vị nào không chỉ đơn giản là nhạo báng bột, chảo và lò nướng, và sau đó khẳng định rằng các phương pháp được gọi trên chúng.

Không hẳn vậy. Nhìn kỹ vào CÁI GÌ chức năng phải làm - nó được cho là đặt ovenđối tượng về một trạng thái cụ thể. Nhìn vào mã, có vẻ như các trạng thái của pandoughcác đối tượng không thực sự quan trọng lắm. Vì vậy, bạn nên truyền một ovenđối tượng (hoặc giả định nó) và khẳng định rằng nó ở trạng thái cụ thể ở cuối lệnh gọi hàm.

Nói cách khác, bạn nên khẳng định rằng bakeCookies()nướng bánh quy .

Đối với các hàm rất ngắn, các bài kiểm tra đơn vị có thể xuất hiện ít hơn so với tautology. Nhưng đừng quên, chương trình của bạn sẽ kéo dài hơn rất nhiều so với thời gian bạn được tuyển dụng viết nó. Chức năng đó có thể hoặc không thể thay đổi trong tương lai.

Kiểm tra đơn vị phục vụ hai chức năng:

  1. Nó kiểm tra rằng mọi thứ hoạt động. Đây là bài kiểm tra đơn vị chức năng ít hữu ích nhất phục vụ và dường như bạn chỉ xem xét chức năng này khi đặt câu hỏi.

  2. Nó kiểm tra xem các sửa đổi trong tương lai của chương trình không phá vỡ chức năng đã được thực hiện trước đó. Đây là chức năng hữu ích nhất của các bài kiểm tra đơn vị và nó ngăn chặn việc đưa các lỗi vào các chương trình lớn. Nó rất hữu ích trong mã hóa thông thường khi thêm các tính năng vào chương trình nhưng nó hữu ích hơn trong việc tái cấu trúc và tối ưu hóa trong đó các thuật toán cốt lõi thực hiện chương trình được thay đổi đáng kể mà không thay đổi bất kỳ hành vi nào có thể quan sát được của chương trình.

Không kiểm tra mã bên trong chức năng. Thay vào đó kiểm tra rằng hàm thực hiện những gì nó nói. Khi bạn nhìn vào các bài kiểm tra đơn vị theo cách này (các hàm kiểm tra, không phải mã) thì bạn sẽ nhận ra rằng bạn không bao giờ kiểm tra các cấu trúc ngôn ngữ hoặc thậm chí logic ứng dụng. Bạn đang kiểm tra một API.


Xin chào, cảm ơn bạn đã trả lời. Bạn có phiền khi nhìn vào bản cập nhật thứ 2 của tôi và đưa ra suy nghĩ của bạn về cách đơn vị kiểm tra chức năng trong ví dụ đó không?
Giô-na

Tôi thấy rằng điều này có thể hiệu quả khi bạn có thể sử dụng lò nướng thật hoặc lò nướng giả, nhưng hiệu quả thấp hơn đáng kể với lò nướng giả. Mocks (bởi các khuyết điểm của Meszaros) không có bất kỳ trạng thái nào, ngoài bản ghi các phương pháp đã được gọi trên chúng. Kinh nghiệm của tôi là khi các chức năng như bakeCookiesđược kiểm tra theo cách này, chúng có xu hướng bị phá vỡ trong quá trình tái cấu trúc sẽ không ảnh hưởng đến hành vi quan sát được của ứng dụng.
James_pic

@James_pic, chính xác. Và vâng, đó là định nghĩa giả mà tôi đang sử dụng. Vì vậy, đưa ra nhận xét của bạn, bạn làm gì trong trường hợp như thế này? Bỏ bài kiểm tra? Viết bài kiểm tra giòn, lặp lại thực hiện nào? Thứ gì khác?
Giô-na

@Jonah Nói chung, tôi sẽ xem xét việc kiểm tra thành phần đó bằng các thử nghiệm tích hợp (Tôi đã thấy các cảnh báo về việc khó gỡ lỗi hơn, có thể là do chất lượng của công cụ hiện đại) hoặc gặp sự cố khi tạo bán giả thuyết phục.
James_pic

3

SaveP People () có nên được kiểm tra đơn vị không, hoặc các kiểm tra như vậy có đủ để kiểm tra cấu trúc ngôn ngữ forEach tích hợp không?

Đúng. Nhưng bạn có thể làm điều đó theo cách chỉ kiểm tra lại công trình.

Điều cần lưu ý ở đây là làm thế nào để chức năng này hoạt động khi một savePersonnửa thất bại? Làm thế nào là nó phải làm việc?

Đó là loại hành vi tinh vi mà hàm cung cấp mà bạn nên thực thi với các bài kiểm tra đơn vị.


Có, tôi đồng ý rằng các điều kiện lỗi tinh vi nên được kiểm tra, nhưng imo đó không phải là một câu hỏi thú vị - câu trả lời là rõ ràng. Do đó, lý do tôi đặc biệt tuyên bố rằng, vì mục đích câu hỏi của tôi, savePeoplekhông nên chịu trách nhiệm xử lý lỗi. Để làm rõ một lần nữa, giả sử rằng chỉsavePeople chịu trách nhiệm lặp qua danh sách và ủy thác việc lưu từng mục vào phương thức khác, liệu nó có còn được kiểm tra không?
Giô-na

@Jonah: Nếu bạn sẽ khăng khăng chỉ giới hạn bài kiểm tra đơn vị của mình cho foreachcấu trúc, và không có bất kỳ điều kiện, tác dụng phụ hoặc hành vi nào bên ngoài nó, thì bạn đã đúng; bài kiểm tra đơn vị mới thực sự không thú vị lắm.
Robert Harvey

1
@jonah - Nó nên lặp đi lặp lại và lưu càng nhiều càng tốt hoặc dừng lại do lỗi? Việc tiết kiệm duy nhất không thể quyết định điều đó, vì nó không thể biết nó được sử dụng như thế nào.
Telastyn

1
@jonah - Chào mừng đến với trang web. Một trong những thành phần chính của định dạng Hỏi và Đáp của chúng tôi là chúng tôi không ở đây để giúp bạn . Câu hỏi của bạn giúp bạn tất nhiên, nhưng nó cũng giúp nhiều người khác đến trang web tìm kiếm câu trả lời cho câu hỏi của họ. Tôi đã trả lời câu hỏi bạn hỏi. Đó không phải là lỗi của tôi nếu bạn không thích câu trả lời hoặc muốn thay đổi các cột gôn. Và thẳng thắn, có vẻ như các câu trả lời khác nói cùng một điều cơ bản, mặc dù hùng hồn hơn.
Telastyn

1
@Telastyn, tôi đang cố gắng hiểu rõ hơn về thử nghiệm đơn vị. Câu hỏi ban đầu của tôi không đủ rõ ràng, vì vậy tôi đang thêm các giải thích để điều khiển cuộc trò chuyện hướng tới câu hỏi thực sự của tôi . Bạn đang chọn giải thích rằng như tôi bằng cách nào đó lừa dối bạn trong trò chơi "đúng đắn". Tôi đã dành hàng trăm giờ để trả lời các câu hỏi về đánh giá mã và SO. Mục đích của tôi là luôn giúp đỡ những người mà tôi đang trả lời. Nếu bạn không, đó là sự lựa chọn của bạn. Bạn không phải trả lời câu hỏi của tôi.
Giô-na

3

Chìa khóa ở đây là quan điểm của bạn về một chức năng cụ thể là tầm thường. Hầu hết các chương trình là tầm thường: gán một giá trị, làm một số phép toán, đưa ra quyết định: nếu thế này thì tiếp tục một vòng lặp cho đến khi ... Trong sự cô lập, tất cả đều tầm thường. Bạn vừa trải qua 5 chương đầu tiên của bất kỳ cuốn sách nào dạy ngôn ngữ lập trình.

Thực tế là viết một bài kiểm tra rất dễ dàng nên là một dấu hiệu cho thấy thiết kế của bạn không tệ đến thế. Bạn có muốn một thiết kế không dễ thử nghiệm?

"Điều đó sẽ không bao giờ thay đổi." là cách mà hầu hết các dự án thất bại bắt đầu. Một bài kiểm tra đơn vị chỉ xác định nếu đơn vị hoạt động như mong đợi trong một tập hợp hoàn cảnh nhất định. Làm cho nó vượt qua và sau đó bạn có thể quên đi các chi tiết thực hiện của nó và chỉ cần sử dụng nó. Sử dụng không gian não cho nhiệm vụ tiếp theo.

Biết mọi thứ hoạt động như mong đợi là rất quan trọng và không tầm thường trong các dự án lớn và đặc biệt là các nhóm lớn. Nếu có một điểm chung mà các lập trình viên có, đó là thực tế tất cả chúng ta đều phải đối phó với mã khủng khiếp của người khác. Ít nhất chúng ta có thể làm là có một số bài kiểm tra. Khi nghi ngờ, hãy viết một bài kiểm tra và đi tiếp.


Cảm ơn bạn đã phản hồi. Điểm tốt. Câu hỏi tôi thực sự muốn trả lời (tôi vừa thêm một bản cập nhật khác để làm rõ) là cách thích hợp để kiểm tra các chức năng không làm gì khác hơn là gọi một chuỗi các dịch vụ khác thông qua ủy quyền. Trong các trường hợp như vậy, có vẻ như các bài kiểm tra đơn vị phù hợp với "tài liệu hợp đồng" chỉ là sự phục hồi của việc thực hiện chức năng, khẳng định rằng các phương thức được gọi trên các giả định khác nhau. Tuy nhiên, bài kiểm tra giống hệt với việc thực hiện trong những trường hợp đó cảm thấy sai ....
Jonah

1

SaveP People () có nên được kiểm tra đơn vị không, hoặc các kiểm tra như vậy có đủ để kiểm tra cấu trúc ngôn ngữ forEach tích hợp không?

Điều này đã được trả lời bởi @BryanOakley, nhưng tôi có thêm một số đối số (tôi đoán):

Đầu tiên, một bài kiểm tra đơn vị là để kiểm tra việc thực hiện hợp đồng, chứ không phải thực hiện API; kiểm tra nên đặt điều kiện tiên quyết sau đó gọi, sau đó kiểm tra hiệu ứng, tác dụng phụ, bất kỳ điều kiện bất biến và điều kiện bài. Khi bạn quyết định kiểm tra cái gì, việc triển khai API không (và không nên) quan trọng .

Thứ hai, bài kiểm tra của bạn sẽ ở đó để kiểm tra các bất biến khi hàm thay đổi . Thực tế bây giờ nó không thay đổi không có nghĩa là bạn không nên thử nghiệm.

Thứ ba, có giá trị trong việc thực hiện một thử nghiệm tầm thường, cả trong cách tiếp cận TDD (bắt buộc nó) và bên ngoài nó.

Khi viết C ++, đối với các lớp của tôi, tôi có xu hướng viết một bài kiểm tra tầm thường để khởi tạo một đối tượng và kiểm tra các bất biến (có thể gán, thường xuyên, v.v.). Tôi thấy thật đáng ngạc nhiên bao nhiêu lần bài kiểm tra này bị hỏng trong quá trình phát triển (ví dụ - bằng cách thêm một thành viên không thể di chuyển trong một lớp, do nhầm lẫn).


1

Tôi nghĩ rằng câu hỏi của bạn sôi lên:

Làm cách nào để kiểm tra đơn vị hàm void mà không phải là kiểm thử tích hợp?

Nếu chúng tôi thay đổi chức năng nướng cookie của bạn để trả lại cookie, ví dụ, việc kiểm tra sẽ trở nên rõ ràng ngay lập tức.

Nếu chúng ta phải gọi pan.GetCookies sau khi gọi hàm mặc dù chúng ta có thể đặt câu hỏi liệu nó 'thực sự là một thử nghiệm tích hợp' hay 'nhưng không phải là chúng ta chỉ kiểm tra đối tượng pan?'

Tôi nghĩ bạn đã đúng khi có các bài kiểm tra đơn vị với mọi thứ bị chế giễu và chỉ kiểm tra các hàm xy và z được gọi là thiếu giá trị.

Nhưng! Tôi sẽ lập luận rằng trong những trường hợp này, bạn nên cấu trúc lại các hàm void của mình để trả về kết quả có thể kiểm tra HOẶC sử dụng các đối tượng thực và thực hiện kiểm tra tích hợp

--- Cập nhật cho ví dụ createNewUser

  • một bản ghi người dùng mới cần được tạo trong cơ sở dữ liệu
  • một email chào mừng cần phải được gửi
  • địa chỉ IP của người dùng cần được ghi lại cho mục đích lừa đảo.

OK vì vậy lần này kết quả của hàm không dễ dàng trả về. Chúng tôi muốn thay đổi trạng thái của các tham số.

Đây là nơi tôi nhận được một chút tranh cãi. Tôi tạo các triển khai giả cụ thể cho các tham số trạng thái

xin vui lòng, độc giả thân mến, hãy thử và kiểm soát cơn thịnh nộ của bạn!

vì thế...

var validatedUserData = new UserData(); //we can use the real object for this
var emailService = new MockEmailService(); //a simple mock which saves sentEmails to a List<string>
var dataStore = new MockDataStore(); //a simple mock which saves ips to a List<string>

//run the test
target.createNewUser(validatedUserData, emailService, dataStore);

//check the results
Assert.AreEqual(1, emailService.EmailsSent.Count());
Assert.AreEqual(1, dataStore.IpsRecorded.Count());
Assert.AreEqual(1, dataStore.UsersSaved.Count());

Điều này tách biệt chi tiết thực hiện của phương pháp được thử nghiệm từ hành vi mong muốn. Một triển khai thay thế:

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.bulkInsedrtUserRecords(new [] {validateduserData});
  emailService.addEmailToQueue(validatedUserData);
  emailService.ProcessQueue();
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

Vẫn sẽ vượt qua bài kiểm tra đơn vị. Thêm vào đó, bạn có lợi thế là có thể sử dụng lại các đối tượng giả qua các bài kiểm tra và cũng đưa chúng vào ứng dụng của bạn để kiểm tra UI hoặc Tích hợp.


Đây không phải là một thử nghiệm tích hợp đơn giản chỉ vì bạn đề cập đến tên của 2 lớp cụ thể ... các thử nghiệm tích hợp là về thử nghiệm tích hợp với các hệ thống bên ngoài, như đĩa IO, DB, các dịch vụ web bên ngoài và v.v. -memory, nhanh chóng, kiểm tra những thứ chúng tôi quan tâm, v.v. Tôi đồng ý rằng phương pháp trả lại cookie trực tiếp cảm thấy như một thiết kế tốt hơn.
sara

3
Chờ đợi. Đối với tất cả những gì chúng ta biết pan.getcookies gửi email đến một đầu bếp yêu cầu họ lấy bánh quy ra khỏi lò khi họ có cơ hội
Ewan

Tôi đoán nó về mặt lý thuyết là có thể, nhưng nó sẽ là một cái tên khá sai lệch. Ai đã từng nghe nói về thiết bị lò nướng đã gửi email? Nhưng tôi thấy bạn điểm, nó phụ thuộc. Tôi giả sử các lớp cộng tác này là các đối tượng lá hoặc chỉ là những thứ trong bộ nhớ đơn giản, nhưng nếu chúng làm những thứ lén lút, thì cần phải thận trọng. Tôi nghĩ rằng việc gửi email chắc chắn nên được thực hiện ở mức cao hơn mức này. điều này có vẻ như logic kinh doanh xuống và bẩn trong các thực thể.
sara

2
Đó là một câu hỏi tu từ, nhưng: "ai đã từng nghe về thiết bị lò nướng đã gửi email?" venturebeat.com/2016/03/08/...
clacke

Xin chào Ewan, tôi nghĩ rằng câu trả lời này đang đến gần nhất với những gì tôi thực sự hỏi. Tôi nghĩ rằng quan điểm của bạn về việc bakeCookiestrả lại các cookie nướng là tại chỗ, và tôi đã có một số suy nghĩ sau khi đăng nó. Vì vậy, tôi nghĩ rằng nó một lần nữa không phải là một ví dụ tuyệt vời. Tôi đã thêm một bản cập nhật nữa với hy vọng cung cấp một ví dụ thực tế hơn về những gì thúc đẩy câu hỏi của tôi. Tôi đánh giá cao đầu vào của bạn.
Giô-na

0

Bạn cũng nên kiểm tra bakeCookies- những gì sẽ / nên e..g bakeCookies(egg, pan, oven)năng suất? Trứng chiên hay ngoại lệ? Theo cách riêng của họ, không phải pancũng không ovenquan tâm đến các thành phần thực tế, vì không ai trong số họ được cho là, nhưng bakeCookiesthường nên mang lại cookie. Tổng quát hơn nó có thể phụ thuộc vào cách doughthu được và cho dù có bất kỳ cơ hội của nó ngày càng trở nên đơn thuần egghoặc ví dụ như waterđể thay thế.

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.