Difference between revisions of "Ru/Introduction to QuickCheck"

From HaskellWiki
< Ru
Jump to navigation Jump to search
 
(15 intermediate revisions by 3 users not shown)
Line 1: Line 1:
 
[[Introduction_to_QuickCheck | Оригинальный текст ]]
 
[[Introduction_to_QuickCheck | Оригинальный текст ]]
Краткое введение в QuickCheck и тестирвание кода в Haskell.
 
   
  +
Краткое введение в QuickCheck и тестирвание кода Haskell.
== Motivation ==
 
   
  +
== Мотивация ==
In September 2006, Bruno Martínez
 
  +
[http://www.haskell.org/pipermail/haskell-cafe/2006-September/018302.html asked]
 
  +
В сентябре 2006г. Bruno Martínez
the following question:
 
  +
[http://www.haskell.org/pipermail/haskell-cafe/2006-September/018302.html задал]
  +
следующий вопрос:
   
 
<haskell>
 
<haskell>
  +
-- Я написал функцию, которая выглядит примерно так
-- I've written a function that looks similar to this one
 
   
 
getList = find 5 where
 
getList = find 5 where
Line 20: Line 21:
 
find n
 
find n
   
  +
-- Я хочу протестировать эту функцию без использования файловой системы.
-- I want to test this function, without hitting the filesystem. In C++ I
 
  +
-- В C++ я бы использовал istringstream. Я не смог найти функцию, которая
-- would use a istringstream. I couldn't find a function that returns a
 
-- Handle from a String. The closer thing that may work that I could find
+
-- возвращает Handle из String. Наиболее близкое из того, что я нашёл и что
  +
-- может сработать, было создать канал и конвертировать файловый дескриптор.
-- was making a pipe and convertind the file descriptor. Can I simplify
 
  +
-- Могу ли я упростить эту функцию, чтобы убрать из нее монаду IO?
-- that function to take it out of the IO monad?
 
  +
 
</haskell>
 
</haskell>
   
  +
Итак, проблема в том как эффективно протестировать эту функцию в Haskell. Решение к которому мы пришли это рефакторинг и QuickTest.
So the problem is: how to effectively test this function in Haskell? The
 
solution we turn to is refactoring and QuickCheck.
 
   
  +
== Сохранение чистоты кода ==
== Keeping things pure ==
 
   
  +
Причина, по которой сложно тестировать getList является монадический код с побочными эффектами, смешанный с чистыми вычислениями, который делает трудным тестирование без полного перевода на модель “черного ящика”, основанного на IO. Такая смесь не подходит для рассуждений о коде.
The reason your getList is hard to test, is that the side effecting monadic code
 
is mixed in with the pure computation, making it difficult to test
 
without moving entirely into a "black box" IO-based testing model.
 
Such a mixture is not good for reasoning about code.
 
   
  +
Давайте распутаем его и затем просто протестируем чистую часть с помощью QuickCheck. Мы можем, для начала, воспользоваться ленивостью IO, чтобы избежать всех неприятных обращений с низкоуровневым IO.
Let's untangle that, and then test the referentially transparent
 
parts simply with QuickCheck. We can take advantage of lazy IO firstly,
 
to avoid all the unpleasant low-level IO handling.
 
   
  +
Итак, первым шагом, вынесем IO часть функции в тонкий слой-оболочку:
So the first step is to factor out the IO part of the function into a
 
thin "skin" layer:
 
   
 
<haskell>
 
<haskell>
  +
-- Тонкий слой монадической оболочки
-- A thin monadic skin layer
 
 
getList :: IO [Char]
 
getList :: IO [Char]
 
getList = fmap take5 getContents
 
getList = fmap take5 getContents
   
  +
-- Собственно работа
-- The actual worker
 
 
take5 :: [Char] -> [Char]
 
take5 :: [Char] -> [Char]
 
take5 = take 5 . filter (`elem` ['a'..'e'])
 
take5 = take 5 . filter (`elem` ['a'..'e'])
 
</haskell>
 
</haskell>
   
== Testing with QuickCheck ==
+
== Тестирование с QuickCheck ==
   
  +
Теперь мы можем протестировать ‘внутренности’ алгоритма, то есть функцию take5, отдельно. Используем QuickCheck. Для начала нам нужно воплощение(instanse) Arbitrary для типа Char -- он занимается генерацией произвольных Char для нашего тестирования. Для простоты я ограничу это промежутком специальных символов:
Now we can test the 'guts' of the algorithm, the take5 function, in
 
isolation. Let's use QuickCheck. First we need an Arbitrary instance for
 
the Char type -- this takes care of generating random Chars for us to
 
test with. I'll restrict it to a range of nice chars just for
 
simplicity:
 
   
 
<haskell>
 
<haskell>
Line 71: Line 62:
 
</haskell>
 
</haskell>
   
  +
Запустим GHCi(или Hugs) и испытаем какие-нибудь обобщенные свойства (хорошо, что мы можем использовать QuickCheck прямо из командной строки Haskell). Сначала, для простоты, значение типа [Char] равно самому себе:
Let's fire up GHCi (or Hugs) and try some generic properties (its nice
 
that we can use the QuickCheck testing framework directly from the
 
Haskell prompt). An easy one first, a [Char] is equal to itself:
 
   
 
<haskell>
 
<haskell>
Line 80: Line 69:
 
</haskell>
 
</haskell>
   
  +
Что произошло? QuickCheck сгенерировал 100 случайных значений [Char] и применил наше свойство, проверяя, что результат был True во всех случаях.
What just happened? QuickCheck generated 100 random [Char] values, and
 
  +
QuickCheck ''сгенерирвал этот тестовый набор для нас''!
applied our property, checking the result was True for all cases.
 
QuickCheck ''generated the test sets for us''!
 
   
  +
Теперь более интересное свойство: двойное обращение тождественно:
A more interesting property now: reversing twice is the identity:
 
   
 
<haskell>
 
<haskell>
Line 91: Line 79:
 
</haskell>
 
</haskell>
   
  +
Великолепно!
Great!
 
   
== Testing take5 ==
+
== Тестирование take5 ==
   
  +
Первое что нужно сделать, это придумать свойства которые являются истинными для всех входных значений. То есть нам нужно найти ''инварианты''.
The first step to testing with QuickCheck is to work out some properties
 
that are true of the function, for all inputs. That is, we need to find
 
''invariants''.
 
   
  +
Простой инвариант может быть таким:
A simple invariant might be:
 
 
<math>\forall~s~.~length~(take5~s)~=~5</math>
 
<math>\forall~s~.~length~(take5~s)~=~5</math>
   
So let's write that as a QuickCheck property:
+
Запишем его, как свойство для QuickCheck:
 
<haskell>
 
<haskell>
 
\s -> length (take5 s) == 5
 
\s -> length (take5 s) == 5
 
</haskell>
 
</haskell>
   
Which we can then run in QuickCheck as:
+
Которое мы можем запустить в QuickCheck так:
 
<haskell>
 
<haskell>
 
*A> quickCheck (\s -> length (take5 s) == 5)
 
*A> quickCheck (\s -> length (take5 s) == 5)
Line 114: Line 100:
 
</haskell>
 
</haskell>
   
  +
А! QuickCheck поймал нас. Если на входе строка, содержащая менее 5 фильтруемых символов, длина строка на выходе будет менее 5. Итак, ослабим немного свойство:
Ah! QuickCheck caught us out. If the input string contains less than 5
 
filterable characters, the resulting string will be less than 5
 
characters long. So let's weaken the property a bit:
 
 
<math>\forall~s~.~length~(take5~s)~\le~5</math>
 
<math>\forall~s~.~length~(take5~s)~\le~5</math>
   
  +
То есть take5 возвращает строку длинной не более 5. Протеcтируем это:
That is, take5 returns a string of at most 5 characters long. Let's test
 
this:
 
 
<haskell>
 
<haskell>
 
*A> quickCheck (\s -> length (take5 s) <= 5)
 
*A> quickCheck (\s -> length (take5 s) <= 5)
Line 126: Line 109:
 
</haskell>
 
</haskell>
   
  +
Хорошо!
Good!
 
   
  +
== Еще одно свойство ==
== Another property ==
 
   
  +
Еще одним свойством для проверки могла могла бы быть корректность возвращаемых символов.То есть, любые возвращённые символы принадлежат множеству ['a','b','c','d','e']
Another thing to check would be that the correct characters are
 
returned. That is, for all returned characters, those characters are
 
members of the set ['a','b','c','d','e'].
 
   
  +
Это можно записать как:
We can specify that as:
 
 
<math>\forall~s~.~\forall~e~.~e~\in~take5~s~\to~e~\in~[abcde] </math>
 
<math>\forall~s~.~\forall~e~.~e~\in~take5~s~\to~e~\in~[abcde] </math>
   
And in QuickCheck:
+
И в QuickCheck:
 
<haskell>
 
<haskell>
 
*A> quickCheck (\s -> all (`elem` ['a'..'e']) (take5 s))
 
*A> quickCheck (\s -> all (`elem` ['a'..'e']) (take5 s))
Line 143: Line 124:
 
</haskell>
 
</haskell>
   
  +
Отлично. Таким образом мы можем иметь некоторую уверенность что функция не возвращает строки ни слишком длинные ни содержащие неправильные символы.
Excellent. So we can have some confidence that the function neither
 
returns strings that are too long, nor includes invalid characters.
 
   
== Coverage ==
+
== Покрытие ==
 
One issue with the default QuickCheck configuration, when testing
 
[Char], is that the standard 100 tests isn't enough for our situation.
 
In fact, QuickCheck never generates a String greater than 5 characters
 
long, when using the supplied Arbtrary instance for Char! We can confirm
 
this:
 
   
  +
Есть одна проблема настройки QuickCheck по умолчанию c тестированием [Char], 100 тестов недостаточно для нашей ситуации.В действительности QuickCheck никогда не сгенерирует строку, содержащую более 5 символов, используя предложенное воплощение Arbtrary для Char. Мы можем проверить это:
 
<haskell>
 
<haskell>
 
*A> quickCheck (\s -> length (take5 s) < 5)
 
*A> quickCheck (\s -> length (take5 s) < 5)
Line 159: Line 134:
 
</haskell>
 
</haskell>
   
  +
QuickCheck тратит своё время, генерируя различные [Char], в то время, как нам просто нужны более длинные строки. Одно из решений для этого, изменить настройки по-умолчанию QuickCheck, чтобы тестировал глубже:
QuickCheck wastes its time generating different Chars, when what we
 
really need is longer strings. One solution to this is to modify
 
QuickCheck's default configuration to test deeper:
 
   
 
<haskell>
 
<haskell>
Line 167: Line 140:
 
</haskell>
 
</haskell>
   
  +
Это указывает системе найти как минимум 10000 тестов до того, как сделать заключение что все в порядке. Давайте проверим, что это генерирует более длинные строки:
This instructs the system to find at least 10000 test cases before
 
concluding that all is well. Let's check that it is generating longer
 
strings:
 
   
 
<haskell>
 
<haskell>
Line 177: Line 148:
 
</haskell>
 
</haskell>
   
  +
Мы можем проверить генерируемые тестовые данные с помощью 'verboseCheck'. Тестирование целочисленных списков:
We can check the test data QuickCheck is generating using the
 
'verboseCheck' hook. Here, testing on integers lists:
 
   
 
<haskell>
 
<haskell>
Line 194: Line 164:
 
</haskell>
 
</haskell>
   
  +
== Двигаясь дальше ==
== Going further ==
 
  +
  +
QuickCheck -- эффективный встроенный язык для тестирования Haskell кода, который позволяет тестировать гораздо более сложные свойства, чем мы здесь увидели. Некоторые источники для дальнейшего чтения:
   
QuickCheck is effectively an embedded domain specific language for
 
testing Haskell code, and allows for much more complex properties than
 
those you've seen here to be tested. Some sources for further reading
 
are:
 
 
* [http://www.cse.unsw.edu.au/~dons/data/QuickCheck.html The QuickCheck source]
 
* [http://www.cse.unsw.edu.au/~dons/data/QuickCheck.html The QuickCheck source]
 
** [http://mathburritos.org/code/darcsweb/browse?r=ghcQC;a=summary QuickCheck GHC batch script]
 
** [http://mathburritos.org/code/darcsweb/browse?r=ghcQC;a=summary QuickCheck GHC batch script]
Line 213: Line 181:
 
* Tutorial: [[QuickCheck / GADT]]
 
* Tutorial: [[QuickCheck / GADT]]
   
  +
Отметим, QuickCheck не является встроенным доменным языком только для тестирования кода ''Haskell''. Создавая instance для Arbitrary для некоторого FFI типа, вы можете использовать Haskell и QuickCheck для проверки кода на других языках.
Note, QuickCheck doesn't need to just be an embedded domain specific language for testing ''Haskell'' code. By making instances of Arbitrary for FFI types you can use Haskell and QuickCheck to check code in other languages.
 
   
[[Category:Tutorials]]
+
[[Category:Ru]]

Latest revision as of 07:45, 2 July 2013

Оригинальный текст

Краткое введение в QuickCheck и тестирвание кода Haskell.

Мотивация

В сентябре 2006г. Bruno Martínez задал следующий вопрос:

-- Я написал функцию, которая выглядит примерно так

getList = find 5 where
     find 0 = return []
     find n = do
       ch <- getChar
       if ch `elem` ['a'..'e'] then do
             tl <- find (n-1)
             return (ch : tl) else
           find n

-- Я хочу протестировать эту функцию без использования файловой системы.  
-- В C++ я бы использовал istringstream. Я не смог найти функцию, которая 
-- возвращает Handle из String. Наиболее близкое из того, что я нашёл и что
-- может сработать, было создать канал и конвертировать файловый дескриптор.
-- Могу ли я упростить эту функцию, чтобы убрать из нее монаду IO?

Итак, проблема в том как эффективно протестировать эту функцию в Haskell. Решение к которому мы пришли это рефакторинг и QuickTest.

Сохранение чистоты кода

Причина, по которой сложно тестировать getList является монадический код с побочными эффектами, смешанный с чистыми вычислениями, который делает трудным тестирование без полного перевода на модель “черного ящика”, основанного на IO. Такая смесь не подходит для рассуждений о коде.

Давайте распутаем его и затем просто протестируем чистую часть с помощью QuickCheck. Мы можем, для начала, воспользоваться ленивостью IO, чтобы избежать всех неприятных обращений с низкоуровневым IO.

Итак, первым шагом, вынесем IO часть функции в тонкий слой-оболочку:

-- Тонкий слой монадической оболочки
getList :: IO [Char]
getList = fmap take5 getContents

-- Собственно работа
take5 :: [Char] -> [Char]
take5 = take 5 . filter (`elem` ['a'..'e'])

Тестирование с QuickCheck

Теперь мы можем протестировать ‘внутренности’ алгоритма, то есть функцию take5, отдельно. Используем QuickCheck. Для начала нам нужно воплощение(instanse) Arbitrary для типа Char -- он занимается генерацией произвольных Char для нашего тестирования. Для простоты я ограничу это промежутком специальных символов:

import Data.Char
import Test.QuickCheck

instance Arbitrary Char where
    arbitrary     = choose ('\32', '\128')
    coarbitrary c = variant (ord c `rem` 4)

Запустим GHCi(или Hugs) и испытаем какие-нибудь обобщенные свойства (хорошо, что мы можем использовать QuickCheck прямо из командной строки Haskell). Сначала, для простоты, значение типа [Char] равно самому себе:

*A> quickCheck ((\s -> s == s) :: [Char] -> Bool)
OK, passed 100 tests.

Что произошло? QuickCheck сгенерировал 100 случайных значений [Char] и применил наше свойство, проверяя, что результат был True во всех случаях. QuickCheck сгенерирвал этот тестовый набор для нас!

Теперь более интересное свойство: двойное обращение тождественно:

*A> quickCheck ((\s -> (reverse.reverse) s == s) :: [Char] -> Bool)
OK, passed 100 tests.

Великолепно!

Тестирование take5

Первое что нужно сделать, это придумать свойства которые являются истинными для всех входных значений. То есть нам нужно найти инварианты.

Простой инвариант может быть таким:

   

Запишем его, как свойство для QuickCheck:

\s -> length (take5 s) == 5

Которое мы можем запустить в QuickCheck так:

*A> quickCheck (\s -> length (take5 s) == 5)
Falsifiable, after 0 tests:
""

А! QuickCheck поймал нас. Если на входе строка, содержащая менее 5 фильтруемых символов, длина строка на выходе будет менее 5. Итак, ослабим немного свойство:

   

То есть take5 возвращает строку длинной не более 5. Протеcтируем это:

*A> quickCheck (\s -> length (take5 s) <= 5)
OK, passed 100 tests.

Хорошо!

Еще одно свойство

Еще одним свойством для проверки могла могла бы быть корректность возвращаемых символов.То есть, любые возвращённые символы принадлежат множеству ['a','b','c','d','e']

Это можно записать как:

И в QuickCheck:

*A> quickCheck (\s -> all (`elem` ['a'..'e']) (take5 s))
OK, passed 100 tests.

Отлично. Таким образом мы можем иметь некоторую уверенность что функция не возвращает строки ни слишком длинные ни содержащие неправильные символы.

Покрытие

Есть одна проблема настройки QuickCheck по умолчанию c тестированием [Char], 100 тестов недостаточно для нашей ситуации.В действительности QuickCheck никогда не сгенерирует строку, содержащую более 5 символов, используя предложенное воплощение Arbtrary для Char. Мы можем проверить это:

*A> quickCheck (\s -> length (take5 s) < 5)
OK, passed 100 tests.

QuickCheck тратит своё время, генерируя различные [Char], в то время, как нам просто нужны более длинные строки. Одно из решений для этого, изменить настройки по-умолчанию QuickCheck, чтобы тестировал глубже:

deepCheck p = check (defaultConfig { configMaxTest = 10000}) p

Это указывает системе найти как минимум 10000 тестов до того, как сделать заключение что все в порядке. Давайте проверим, что это генерирует более длинные строки:

*A> deepCheck (\s -> length (take5 s) < 5)
Falsifiable, after 125 tests:
";:iD^*NNi~Y\\RegMob\DEL@krsx/=dcf7kub|EQi\DELD*"

Мы можем проверить генерируемые тестовые данные с помощью 'verboseCheck'. Тестирование целочисленных списков:

*A> verboseCheck (\s -> length s < 5)
0: []
1: [0]
2: []
3: []
4: []
5: [1,2,1,1]
6: [2]
7: [-2,4,-4,0,0]
Falsifiable, after 7 tests:
[-2,4,-4,0,0]

Двигаясь дальше

QuickCheck -- эффективный встроенный язык для тестирования Haskell кода, который позволяет тестировать гораздо более сложные свойства, чем мы здесь увидели. Некоторые источники для дальнейшего чтения:

Отметим, QuickCheck не является встроенным доменным языком только для тестирования кода Haskell. Создавая instance для Arbitrary для некоторого FFI типа, вы можете использовать Haskell и QuickCheck для проверки кода на других языках.