۱۴ - ۳تستینگ متداول
با کتابخونه ِ hspec
یه مورد تست رو نشون میدیم، اما hspec
رو کامل توضیح نمیدیم. این فصل به شما یاد میده چطور برای کُدهای آیندهتون تست بنویسین، اما برای این کار، درکِ جزئیاتِ نحوهی کارِ کتابخونه واجب نیست. یه سری مفاهیمی که hspec
باهاشون کار میکنه، مثل فانکتور، اَپلیکِتیو، و مونَد رو در فصلهای مستقلِ خودشون توضیح میدیم.
اول یه مورد تست برای جمع مینویسیم. در کل بهتره یه پروژهی Cabal بسازیم، حتی برای آزمایشهای کوچیک. اگه یه پروژهی دائمی برای آزمایشهاتون داشته باشین، کمتر مجبور میشین یه سری کارهای تکراری رو انجام بدین. ما فرض میکنیم هنوز چنین پروژهای ندارین و یه پروژهی کوچیک شروع میکنیم:
-- addition.cabal
name: addition
version: 0.1.0.0
license-file: LICENSE
author: Chicken Little
maintainer: sky@isfalling.org
category: Text
build-type: Simple
cabal-version: >=1.10
library
exposed-modules: Addition
ghc-options: -Wall -fwarn-tabs
build-depends: base >=4.7 && <5
, hspec
hs-source-dirs: .
default-language: Haskell2010
دقت کنین که برای وابستگی ِ hspec
هیچ بازهی نسخهای تعریف نکردیم. معمولاً باید از جدیدترینش استفاده کرد، اما فعلاً اشکال نداره نسخهش رو مشخص نکنیم.
بعد یه ماژول ِ Addition
(ماژولهای افشا شده یا exposed-modules
) در همون پوشهای که فایلِ Cabal قرار داره درست میکنیم. معنیِ اون نقطهای که جلوی hs-source-dirs
گذاشتیم همینه – با نقطه به آدرس فعلی اشاره میکنیم.
فعلاً یه تابعِ جاپُرکن مینویسیم تا از کارکردِ همه چیز مطمئن بشیم:
-- Addition.hs
module Addition where
sayHello :: IO ()
sayHello = putStrLn "hello!"
یه فایلِ خالی به اسمِ LICENSE
هم درست میکنیم که دستورِ build
بِهمون گیر نَده:
$ touch LICENSE
قبل از اجرای دستورهای Stack، آدرس ِ پروژهتون باید چنین شکلی باشه:
$ tree
.
├── Addition.hs
├── addition.cabal
└── LICENSE
حالا فایل Stack رو راهاندازی میکنیم تا اسنپشات ِ Stackage که استفاده میکنیم رو توصیف کنه:
$ stack init
بعد هم پروژهمون رو میسازیم که وابستگیهای لازم رو هم نصب میکنه:
$ stack build
اگه کار کرد، آتیشِ REEEEEEEPL رو روشن کنیم و ببینیم سلام کردن بلده یا نه:
$ stack ghci
[یه کم شلوغی از تنظیمات و بارگذاری و غیره]
Ok, modules loaded: Addition.
Prelude> sayHello
hello!
اگه تا اینجا رسیدین یعنی یه بسترِ آماده برای تستینگ با hspec
دارین!
حقیقت به نقل از Hspec
حالا ماژول ِ اصلی hspec
رو وارد میکنیم:
module Addition where
import Test.Hspec
sayHello :: IO ()
sayHello = putStrLn "hello!"
دقت کنین که همهی واردات باید بعد از تعریفِ ماژول، و قبل از اولین بیانیهای که تعریف میشه بیان. تا اینجا ممکنه به خطاهایی خورده باشین. دو نمونهشون رو مثال میزنیم:
module Addition where
sayHello :: IO ()
sayHello = putStrLn "hello!"
import Test.Hspec
اینجا واردات رو بعد از حداقل یه تعریف نوشتیم. کامپایلر این اشتباهِ بخصوص رو نمیشناسه، به همین خاطر هم نمیتونه دقیقاً اشکالِ کار رو تشخیص بده:
Prelude> :r
[1 of 1] Compiling Addition
Addition.hs:7:1: parse error on input ‘import’
Failed, modules loaded: none.
دیگه چه اشتباهی میشه کرد؟ مثلاً ممکنه پکیج ِ hspec
رو نصب کرده باشیم، اما در build-depends
ِ پروژهمون ننوشته باشیمش. دقت کنین اگه فایلِ .cabal
ِتون رو تغییر دادین (مثلاً برای بازسازی ِ این خطا، یا مشکلی بوده که برطرف کردین)، باید اول از REPL بیان بیرون و دوباره اجراش کنین:
$ stack build
{... شلوغی ...}
Could not find module ‘Test.Hspec’
It is a member of the hidden package
‘hspec-2.2.3@hspec_JWyjr3DNMsw1kiPzf88M5w’.
Perhaps you need to add ‘hspec’ to the
build-depends in your .cabal file.
Use -v to see a list of the files searched for.
{... بازم شلوغی ...}
Process exited with code: ExitFailure 1
اگه تغییراتی دادین تا خودتون این خطاها رو ببینین، دوباره hspec
رو به build-depends
اضافه و نصبش کنین. وقتی hspec
جزئی از وابستگیها باشه، stack build
درست کار میکنه.
با فرضِ اینکه همه چیز سرِ جاشه و Test.Hspec
وارد میشه، میتونیم با دستورِ :browse
یه لیست از تایپهای ماژولها بگیریم و نگاهِ کلی به چیزهایی که در اختیار میذاره بندازیم:
Prelude> :browse Test.Hspec
context :: String -> SpecWith a -> SpecWith a
example :: Expectation -> Expectation
specify :: Example a => String -> SpecWith (Arg a)
(... لیستِ درازیه ...)
Prelude>
اگه یه کم با کتابخونه و طرزِ کارش آشنایی داشته باشین، :browse
کاربردش بیشتره. مستنداتِ یه کتابخونهای که نمیشناسین بیشتر از این دستور میتونن کمک کنن. اگه مستندات خوب نوشته شده باشه، طرزِ کارِ بخشهای مهمِ کتابخونه رو توضیح میده، چندتا مثال هم میزنه. در مقابله با مفاهیمِ جدید، این بهترین گزینهست. hspec
هم مستندات ِ خیلی خوبی روی سایتش داره.
اولین تستِ Hspec
حالا یه تست به ماژول ِمون اضافه میکنیم. اگه یه نگاه به مستنداتش بندازین، متوجه میشین که مثالِ ما خیلی جذاب نیست، اما جلوتر بهتر میشه:
module Addition where
import Test.Hspec
main :: IO ()
main = hspec $ do
describe "Addition" $ do
it "1 + 1 is greater than 1" $ do
(1 + 1) > 1 `shouldBe` True
هم به زبان انگلیسی و هم در کُد، اعلام کردیم که (۱ + ۱) باید بزرگتر از ۱ باشه، و این چیزیه که hspec
برامون تست میکنه. شاید نوشتار ِ do
رو از فصلِ قبل یادتون بیاد. اونجا هم گفتیم که از این گرامر برای متسلسل کردنِ اجراییههای مونَدیک استفاده میشه. البته مونَدی که اونجا باهاش کار میکردیم، IO
بود.
اینجا چندتا بلوک ِ do
رو تودرتو کردیم. تایپهای این بلوکهای do
که به hspec
، describe
، و it
دادیم، IO ()
نیستن، بلکه یه چیزی مختص به hspec
اند. در انتها جوابِ IO ()
میدن، اما مونَدهای دیگهای هم دَرگیرند. هنوز موندها رو نگفتیم، اما بدون درکِ دقیقِ طرز کارشون هم میشه پیش رفت.
احتمالاً به خاطر تبدیلِ پیشفرضیِ لفظهای Num a => a
به Integer
(م. همون type defaulting که در فصلِ تایپکلاسها گفتیم)، یه هشدار میگیرین. هم میشه نادیده بگیرینش، هم میتونین یه تایپ سیگنچر اضافه کنین، هرطور دوست دارین. با کُدِ بالا، ماژول ِمون رو بارگذاری میکنیم و با اجرای main
نتیجهی تستهامون رو میبینیم:
Prelude> main
Addition
1 + 1 is greater than 1
Finished in 0.0041 seconds
1 example, 0 failures
خب، اینجا چه اتفاقی افتاد؟ hspec
کُدتون رو اجرا، و تأیید کرد که آرگومانهایی که به shouldBe
دادین با هم برابراند.* یه نگاه به تایپها بندازیم:
shouldBe :: (Eq a, Show a)
=> a -> a -> Expectation
-- تفاوت بین این دوتا رو ببینین
(==) :: Eq a => a -> a -> Bool
م. ترکیبِ should be معنای "باید ... باشه" رو میده. پس (1+1)>1 `shouldBe` True
رو میشه اینطور خوند: (1+1)>1
باید True
باشه.
میشه گفت اون تابعِ shouldBe
همون ==
ِه که در دنیای hspec
شبیهسازی شده. نمونه ِ Show
لازم داره تا یه مقدار رو نشون بده. یعنی نمونه ِ Show
به hspec
این امکان رو میده که نتیجهی تستها رو نشون بده، نه فقط یه Bool
.
یه تستِ دیگه اضافه میکنیم. یه کم فرق داره:
main :: IO ()
main = hspec $ do
describe "Addition" $ do
it "1 + 1 is greater than 1" $ do
(1 + 1) > 1 `shouldBe` True
it "2 + 2 is equal to 4" $ do
2 + 2 `shouldBe` 4
بلوک ِ describe
رو مثلِ بالا تغییر بدین و بعد تو REPL اجراش کنین:
Prelude> main
Addition
1 + 1 is greater than 1
2 + 2 is equal to 4
Finished in 0.0004 seconds
2 example, 0 failures
واسه خوشگذرونی، یه تستِ hspec
برای یکی از چیزهایی که قبلاً از کتاب نوشتین مینویسیم. در فصل توابعِ بازگشتی، تابعِ تقسیمِ خودمون رو اینطوری نوشتیم:
dividedBy :: Integral a => a -> a -> (a, a)
dividedBy num denom = go num denom 0
where go n d cound
| n < d = (cound, n)
| otherwise = go (n - d) d (cound + 1)
میخوایم تست کنیم ببینیم همونطور که باید کار میکنه یا نه. برای سادهتر شدن، dividedBy
رو به فایلِ Addition.hs
اضافه کردیم و تستهای hspec
که داشتیم رو بازنویسی کردیم. میخوایم مطمئن شیم این تابع تعداد دفعاتی که تفریق میکنه، و باقیماندهای که برمیگردونه درست هستن یا نه، پس دوتا چیز رو برای تستِ hspec
تعیین میکنیم:
main :: IO ()
main = hspec $ do
describe "Addition" $ do
it "15 divided by 3 is 5" $ do
dividedBy 15 3 `shouldBe` (5, 0)
it "22 divided by 5 is\
\ 4 remainder 2" $ do
dividedBy 22 5 `shouldBe` (4, 2)
همین. حالا اگه Addition.hs
رو در REPL بارگذاریِ مجدد کنیم، میتونیم تابعِ تقسیممون رو تست کنیم:
*Addition> main
Addition
15 divided by 3 is 5
22 divided by 5 is 4 remainder 2
Finished in 0.0012 second
2 examples, 0 failures
هورا! ریاضی بَلدیم!
آنتراکت: تمرین کوتاه
در تمرینهای آخر فصلِ توابعِ بازگشتی، یکی از تمرینها مشابه این بود:
تابعی بنویسین که دو عدد رو با جمعِ بازگشتی در هم ضرب کنه. تایپش (Eq a, Num a) => a -> a -> a
میشه، البته بسته به رَوشِ حلتون، ممکنه تایپکلاسِ Ord
هم لازم داشته باشین.
اگه هنوز جوابتون رو دارین که چه بهتر! اگر هم ندارین، دوباره بنویسین و بعد هم براش تستهای hspec
تعریف کنین.
مثالهایی که بالا زدیم، مبانیِ نوشتن تست برای بررسیِ مقادیر بخصوص رو نشون میدن. اگه یه مثال پختهتر میخواین، میتونین به کتابخونه ِ کریس، به اسم Bloodhound مراجعه کنین.