Skip to main content

Kiểm định tích hợp

Chúng ta đã thấy cách chúng ta có thể viết các bài kiểm tra cho hợp đồng thông minh của mình một cách thuận tiện bằng cách sử dụng trình bao (wrapper) của chúng ta dựa trên mô hình đơn giản của Plutus. Nhưng các thử nghiệm này đang chạy trên một sổ cái giả, nghĩa là chúng ta thực sự chỉ đang mô phỏng nó bằng cách có một số cấu trúc dữ liệu giả (chẳng hạn như bộ UTxO) đang được cập nhật khi gửi giao dịch thành công. Tuy nhiên, chúng ta có thể viết các bài kiểm tra để kiểm tra node thực và lập trình thuận tiện hơn một chút bằng cách tạo ra mạng riêng của chúng ta (viết tắt là privnet). Đây là bảng phác thảo sự khác biệt giữa hai cách tiếp cận:

Thử nghiệm sử dụng PSM WrapperKiểm tra bằng Mạng riêng
Chạy chống lại sổ cái giảChạy với node thực
Mỗi bài kiểm tra đơn vị nhận được bộ ví mới (có số dư ban đầu)Mỗi bài kiểm tra đơn vị tiếp theo tiếp tục dựa trên các hiệu ứng gây ra bởi những bài kiểm tra trước đó
Nhanh chóng, tinh khiết hơn và thuận tiệnChậm như mỗi khe là 0,1 giây

Do đó, các thử nghiệm này phù hợp với thử nghiệm tích hợp.

Đưa lên mạng riêng

Mạng riêng của chúng ta được điều chỉnh từ WoofPool's cardano-private-testnet-setup kho.

Để xoay nó lên:

  1. Sao chép cái kho này. Đảm bảo không sao chép nó trong một số đường dẫn lồng sâu vì khi đó độ dài đường dẫn tới tệp ổ cắm được tạo ( node.sock ) có thể vượt quá 108 ký tự 1 .
  2. Nhập nó và geniusyield chi nhánh thanh toán.
  3. Nhập nội dung sau vào thiết bị đầu cuối: ./scripts/automate.sh (bạn cần phải có cardano-nodecardano-cli sẵn có trong tệp PATH ).

Sau khi thông báo "Congrats! Your network is ready for use!", bạn có thể thử chạy thử nghiệm (trong một thiết bị đầu cuối khác).

Đầu tiên, giả sử đường dẫn đến private-testnet-simple là X , sau đó nằm trong thư mục dự án mẫu của bạn, bạn có thể thực hiện các kiểm tra bằng cách chạy GENIUSYIELD_PRIVNET_DIR=$X/private-testnet cabal run betref-privnet-tests -- -j1

Điều này -j1 là cần thiết để các bài kiểm tra chạy tuần tự.

note

Hãy nhớ dừng ( CTRL-C , và killall cardano-node ) mạng thử nghiệm riêng tư, nếu không nó sẽ chiếm hết dung lượng đĩa của bạn.

Cách chúng ta thiết lập nó cho bản mẫu thử nghiệm của mình là chúng ta có ba người dùng trong đó người dùng thứ hai và thứ ba có số dư sau:

  • 5 UTxO mỗi cái chứa hàng nghìn ada
  • 1 triệu mỗi thẻ vàng và sắt

Danh sách UTxO "Người dùng 1" không rõ ràng nhưng bao gồm 5 UTxO, mỗi UTxO chứa khoảng một nghìn ada cộng với một số UTxO khác chứa nhiều ada hơn. Ngoài ra, Người dùng 1 có 3 triệu mỗi mã thông báo vàng và sắt.

Chúng ta sẽ sớm xem cách tạo người dùng mới.

danger

Trừ khi bạn tắt và khởi động lại mạng riêng, chạy lại các bài kiểm tra mạng riêng của mình, chúng sẽ chạy ở trạng thái mạng đã sửa đổi. Vì vậy, nói chung, nếu bạn muốn thực hiện lại lệnh đã đề cập trước đó, viz. ATLAS_PRIVNET_DIR=$(pwd)/private-testnet-simple/private-testnet cabal run privnet-tests -- -j1 , trước tiên bạn nên khởi động lại privnet 2 .

Hiểu bài kiểm tra đầu tiên của chúng ta

note

Các bài kiểm tra được viết trong này tập tin và đang được gọi ở đây.

Đây là mã (& giải thích theo sau nó):

  testCaseSteps "Balance checks & taking pot by closest guesser should pass" $ \info -> withSetup setup info $ \ctx -> do

-- First step: Construct the parameters and obtain validator from it.
--
-- Let's define a new User to represent Oracle (not necessary though)
oracleUser <- newTempUserCtx ctx User1 (valueFromLovelace 20_000_000)
(currentSlot, slotConfig) <- getSlotAndConfig ctx
let betUntilSlotDelta = 100
betRevealSlotDelta = 200
betUntilTime = slotToBeginTimePure slotConfig (unsafeAdvanceSlot currentSlot betUntilSlotDelta)
betRevealTime = slotToBeginTimePure slotConfig (unsafeAdvanceSlot currentSlot betRevealSlotDelta)
brp = BetRefParams (pubKeyHashToPlutus $ userPkh oracleUser) (timeToPlutus betUntilTime) (timeToPlutus betRevealTime) (valueToPlutus $ valueFromLovelace 10_000_000)
validator = betRefValidator' brp
validatorAddress <- ctxRunC ctx User1 $ betRefAddress brp
-- Second step: Putting reference script for validator.
refScript <- addRefScriptCtx ctx User1 (validatorToScript validator)
threadDelay 1_000_000
-- Third step: Put some bets.
--
-- 1st bet.
txBodyLock <- ctxRunI ctx User1 $ placeBet refScript brp (OracleAnswerDatum 1) (valueFromLovelace 10_000_000) (ctxUserAddr ctx User1) Nothing
lockedORef <- findOutput validatorAddress txBodyLock
void $ submitTx ctx User1 txBodyLock
threadDelay 1_000_000

--
-- 2nd bet.
txBodyLock <- ctxRunI ctx User2 $ placeBet refScript brp (OracleAnswerDatum 2) (valueFromLovelace 20_000_000) (ctxUserAddr ctx User2) (Just lockedORef)
lockedORef <- findOutput validatorAddress txBodyLock
void $ submitTx ctx User2 txBodyLock
threadDelay 1_000_000
--
-- 3rd bet.
txBodyLock <- ctxRunI ctx User1 $ placeBet refScript brp (OracleAnswerDatum 3) (valueFromLovelace 35_000_000) (ctxUserAddr ctx User1) (Just lockedORef)
lockedORef <- findOutput validatorAddress txBodyLock
void $ submitTx ctx User1 txBodyLock
threadDelay 1_000_000

-- Fourth step, get the bets pot.
--
-- Let's first wait for the required amount
ctxWaitUntilSlot ctx (unsafeAdvanceSlot currentSlot betRevealSlotDelta) -- here this `currentSlot` is what we obtained sometime ago, the actual current slot has certainly increased a lot by now.
--
-- Let's then add for the reference input
refInputORef <- addRefInputCtx ctx User1 True (userAddr oracleUser) (datumFromPlutusData (OracleAnswerDatum 2))
threadDelay 1_000_000
--
-- Balance of `User2` before unlocking
balance <- ctxQueryBalance ctx User2
--
-- Unlock operation
txBodyUnlock <- ctxRunI ctx User2 $ takeBets refScript brp lockedORef (ctxUserAddr ctx User2) refInputORef
void $ submitTx ctx User2 txBodyUnlock
threadDelay 1_000_000
--
-- Balance of `User2` after unlocking
balance' <- ctxQueryBalance ctx User2
let diff = valueMinus balance' balance
adaChange = fst $ valueSplitAda diff -- should be withing [65 ada - max-fee, 65 ada)
adaExpectedIncrease = 65_000_000
maxFee = 1_000_000
assertBool "User2 ada increase must be b/w [adaExpectedIncrease - maxFee, adaExpectedIncrease)"
$ adaExpectedIncrease - maxFee <= adaChange && adaChange < adaExpectedIncrease

Dòng đầu tiên testCaseSteps "test description" $ \info -> withSetup setup info $ \ctx -> do có thể được coi là bản soạn sẵn cho tất cả các bài kiểm tra của bạn.

ctx biểu thị cái gọi là ngữ cảnh (thuộc loại Ctx ) và chứa thông tin về người dùng của chúng ta, mã thông báo bổ sung, v.v. Nó được định nghĩa trong Ctx.hs tệp và điều cần thiết là phải xem qua tệp đó nếu bạn định viết các bài kiểm tra này.

Biến info được sử dụng để ghi nhật ký tin nhắn và bạn có thể sử dụng nó trong do khối thử nghiệm của mình như info $ printf "Hello from %s" "Atlas"

Tiếp theo chúng ta thấy việc sử dụng newTempUserCtx chức năng tiện ích. Như đã đề cập trước đó, chúng ta đã có ba người dùng trong ngữ cảnh của mình, nơi họ có loại User :

data User = User
{ userSKey :: !GYPaymentSigningKey
, userAddr :: !GYAddress
, userColl :: !GYTxOutRef
}

Chúng ta cũng có một loại UserIdx được xác định cho ba người dùng này:

data UserIdx
= User1
| User2
| User3

Nhưng vào những thời điểm hiếm hoi, chúng ta có thể cần tạo một người dùng mới. Người dùng như vậy sẽ không phải là một phần của bối cảnh và do đó sẽ là địa phương để thử nghiệm tạo ra nó 3 .

Chúng ta có thể làm điều đó với sự trợ giúp của newTempUserCtx chức năng. Nó chấp nhận tham số bối cảnh, chỉ mục cho người dùng sẽ tài trợ cho người dùng mới này và giá trị được cung cấp cho người dùng mới này. Lưu ý rằng bên trong nó sẽ tạo hai UTxO cho người dùng mới này, trong đó một UTxO sẽ là tài sản thế chấp và sẽ lấy 5 ada từ giá trị đã cho.

Tiếp theo chúng ta thấy việc sử dụng getSlotAndConfig chức năng. Trước đây khi chúng ta viết cho các bài kiểm tra PSM, chúng ta có thể làm việc trong các vị trí tuyệt đối vì chúng ta luôn chạy từng bài kiểm tra từ đầu sổ cái nhưng đây không phải là trường hợp. Vì vậy, chúng ta sẽ cần phải làm việc với các vị trí tương đối, nghĩa là chúng ta tìm vị trí hiện tại và sau đó thêm độ lệch đối với nó. Hàm getSlotAndConfig có định nghĩa sau:

getSlotAndConfig :: Ctx -> IO (GYSlot, GYSlotConfig)
getSlotAndConfig ctx = do
slot <- ctxCurrentSlot ctx
sc <- ctxSlotConfig ctx
return (slot, sc)

Tiếp theo, chúng ta tính toán các tham số hợp đồng của mình và vì chúng ta đã có được cấu hình vị trí, chúng ta có thể sử dụng slotToBeginTimePure thay vì slotToBeginTime .

Tiếp theo chúng ta thấy việc sử dụng ctxRunC . Để hiểu nó, trước tiên chúng ta cần xem xét chữ ký của ctxRunF .

ctxRunF :: forall t v. Traversable t => Ctx -> UserIdx -> ReaderT GYCompiledScripts GYTxMonadNode (t (GYTxSkeleton v)) -> IO (t GYTxBody)

Chúng ta thấy rằng nó có một biến kiểu t nên có một thể hiện là Traversable . Hai chức năng khác, cụ thể là ctxRunCctxRunI gọi ctxRunF chức năng này với sự khởi tạo thích hợp của loại biến t .

Đây là bảng giải thích về ba hàm liên quan ( ctxRunFctxRunCctxRunI ) này:

Chức năngKhi nào thì sử dụng?Nó làm gì?
ctxRunIKhi bạn muốn xây dựng cho đơn GYTxSkeletonNó bao bọc bộ xương của chúng ta dưới Identity 4 , đó là hậu tố I đại diện cho
ctxRunFKhi bạn nói nhiều bộ xương, như [GYTxSkeleton] hoặc Maybe GYTxSkeleton-
ctxRunCKhi bạn không muốn xây dựng bộ xương. Điều này đặc biệt hữu ích cho các hoạt động như utxosAtAddressHàm tạo kiểu Const được định nghĩa là newtype Const a b = Const { getConst :: a } và do đó tham số kiểu b là ảo và do đó hàm này giúp chúng ta bỏ qua cho GYTxSkeleton

Tiếp theo, chúng ta thêm tập lệnh tham chiếu bằng chức năng tiện ích của trình trợ giúp addRefScriptCtx .

Sau đó, chúng ta bắt đầu đặt cược, khi chúng ta có nội dung giao dịch, chúng ta sử dụng findOutput chức năng cung cấp cho chúng ta tham chiếu đến UTxO (cái đầu tiên nó tìm thấy 5 ) đang bị khóa tại địa chỉ tập lệnh. Chúng ta sử dụng threadDelay 1_000_000 để đợi một giây để giao dịch của chúng ta được thêm vào chuỗi khối.

Sau khi đặt cược, chúng ta ctxWaitUntilSlot đợi đến thời điểm mở khóa.

Tiếp theo, chúng ta thêm đầu vào tham chiếu của mình bằng addRefInputCtx chức năng tiện ích của trình trợ giúp.

Bây giờ trước khi thực hiện thao tác unlock chúng ta truy vấn số dư của unlocker để sau này so sánh với nó bằng ctxQueryBalance hàm.

Tiếp theo, chúng ta thực hiện thao tác mở khóa (gọi takeBets thao tác của chúng ta).

Cuối cùng, chúng ta xác minh rằng người mở khóa có thể lấy tất cả các cược bằng cách so sánh số dư.

note

Có tồn tại một chức năng trợ giúp trong Asserts.hs đối với những gì chúng ta đã làm trong bước cuối cùng, tức là để thấy rằng giá trị kỳ vọng của chúng ta phù hợp với phí xem xét thực tế, đây là định nghĩa của nó:

{- Asserts if the user funds change as expected.
The `fees` argument is an estimation for the transaction fees.
-}
assertUserFunds :: Integer -> Ctx -> UserIdx -> GYValue -> IO ()
assertUserFunds fees ctx uid expectedValue = do
currentValue <- ctxQueryBalance ctx uid
let (cLovelace, cAssets) = valueSplitAda currentValue
(eLovelace, eAssets) = valueSplitAda expectedValue
assertBool (unwords ["The non-Ada token didn't change as expected",
"\nExpected: ", show eAssets,
"\nCurrent: ", show cAssets])
(cAssets == eAssets)
assertBool (unwords ["The lovelaces didn't change as expected,",
"\nExpected: ", show eLovelace,
"\nCurrent: ", show cLovelace])
((cLovelace + fees >= eLovelace) &&
(cLovelace < eLovelace))

Viết một bài kiểm tra thất bại

Bây giờ, hãy xem một thử nghiệm khác trong đó chúng ta sửa đổi một chút bước cuối cùng (tất cả các phần còn lại đều giống nhau) và thay vào đó, lần này chúng ta cố gắng lấy tiền mà không phải người đoán gần nhất.

  -- Fourth step, get the bets pot.
--
-- Let's first wait for the required amount
ctxWaitUntilSlot ctx (unsafeAdvanceSlot currentSlot betRevealSlotDelta) -- here this `currentSlot` is what we obtained sometime ago, the actual current slot has certainly increased a lot by now.
--
-- Let's then add for the reference input
refInputORef <- addRefInputCtx ctx User1 True (userAddr oracleUser) (datumFromPlutusData (OracleAnswerDatum 2))
threadDelay 1_000_000
--
-- Unlock operation
-- But this time by wrong guesser
assertThrown isTxBodyErrorAutoBalance $ ctxRunI ctx User1 $ takeBets refScript brp lockedORef (ctxUserAddr ctx User1) refInputORef

Lưu ý rằng chúng ta thử bắt lỗi bằng assertThrown chức năng. Ở đây isTxBodyErrorAutoBalance được định nghĩa là (cả cái này & assertThrown có định nghĩa của chúng trong Asserts.hs tài liệu):

isTxBodyErrorAutoBalance :: BuildTxException -> Bool
isTxBodyErrorAutoBalance (BuildTxBodyErrorAutoBalance _) = True
isTxBodyErrorAutoBalance _ = False

Do đó, assertThrown chức năng của chúng ta kiểm tra hai điều:

  1. Cho dù hành động của chúng ta thực sự làm tăng một ngoại lệ.
  2. Nếu một ngoại lệ được nêu ra, liệu nó có thỏa mãn vị từ của chúng ta không? Chẳng hạn, ở đây vị ngữ của chúng ta là isTxBodyErrorAutoBalance .
note

Bạn cũng có thể bắt IO lỗi như:

  errored <- catchIOError (submitTx ctx User1 txBody >> pure False) (\_ -> pure True)
unless errored $ assertFailure "Expecting an IOError exception"

Với điều này, chúng ta kết luận khi viết các bài kiểm tra tích hợp.

Chú thích

  1. https://unix.stackexchange.com/q/367008
  2. Để thuận tiện, bạn có thể viết một tập lệnh bash kết hợp thiết lập, chạy thử nghiệm và đóng tất cả privnet thành một tập lệnh đơn giản.
  3. Mặc dù người dùng này là cục bộ của thử nghiệm đã tạo ra nó, nhưng nó vẫn sẽ tồn tại trong mạng riêng của chúng ta.
  4. Về mặt kỹ thuật, không phải trình bao (wrapper) đang diễn ra ở đây mà đúng hơn là chúng ta ép buộc với Identity newtype.
  5. Do đó, chức năng này được sử dụng khi chúng ta chỉ tạo một đầu ra duy nhất cho một địa chỉ bên ngoài.

Picture