۱۴ - ۳تستینگ متداول
با کتابخونه ِ 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 مراجعه کنین.