۱۴ - ۳تستینگ متداول

با کتابخونه ِ ‏‎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 مراجعه کنین.