LINUX.ORG.RU

Функциональные языки и unit-тестирование

 , ,


1

4

Здравствуйте. Как обстоят дела с unit-тестированием в функциональных языках? Интересуют в первую очередь «чистые» языки (Haskell).

Вот в ОО-языках, например, есть такой подход — Mock Object. Объекты, которые имитируют поведение реальных компонентов системы (БД, сеть и т.п.) Это позволяет полноценно тестировать систему вне production environment. Для Java есть полдюжины mock-фреймворков. Но, поскольку в «чистых» функциональных языках даже такого понятия как «объект» нет, то сия парадигма неприменима.

Так каким же образом принято реализовывать сабж? Спасибо.

Чо там тестировать? открыл oeis a000142, сверился - готово.

чтобы вы не подумали, что я недооцениваю мощь хаскеля, то можно еще и a000045!

cdshines ★★★★ ()

А зачем костылить какие-то непонятные объекты если в «чистых функциональных языках» можно просто позвать функции в нужном порядке?

anonymous ()

Поддержу топикстартера и задамся следующим вопросом.

Как, например, смоделировать следующие события в формате теста ?: есть сервер, есть клиент; стартует сервер, стартует 10000 клиентов, клиенты срут командочкой в сервер, клиенты дохнут; проверить, что сервер остался живым.

dmitry_malikov ★★ ()
Ответ на: комментарий от anonymous

Ты не понял, mock-объекты «прикидываются» реальными ресурсами, которые есть в продакшене: СУБД, сеть, таймер, файлы. Причём умеют эмулировать нерегулярность в поведении (случайные ошибки и т.п.)

Вот, скажем, продакшеновая БД находится в DMZ и тебя к ней не подпускают. В каком порядке прикажешь вызывать функции?

Или, например, программа должна каждый час выполнять некое действие. И что, ты будешь каждый раз ждать нового часа? Или таймер подкручивать? Mock-объект подсовывает программе фейковый таймер, который может показать то, что мы его попросим.

Похоже, ты просто не знаешь, что такое mock-объекты, и поэтому мелешь херню про «позвать функции в нужном порядке».

anonymous ()

продакшен
DMZ
СУБД
сервер
клиент
сеть
файлы
таймеры

Фи, экая дурнопахнущая куча баззвордов. Вы ничего не поняли. Haskell не предназначен для подобного рода низменных задач, он работает по-другому.

anonymous ()

Но, поскольку в «чистых» функциональных языках даже такого понятия как «объект» нет

Ну и что есть такого, что можно реализовать с помощью «объектов» и нельзя с помощью классов типов? Особенно применительно к mock-тестированию?

pitekantrop ★★★ ()
Ответ на: комментарий от anonymous

Анон,

Haskell не предназначен

Ты либо туп, либо Хаскель не ЯОН. Во второе не верится.

d_Artagnan ★★ ()

Чистые функциональные языки потенциально должны быть более тестируемыми. Потенциально, но вопрос правильно поставлен. Тем более как мы подключаем компонент? Если передаем как параметр в функции, то вопрос с mockом отпадает, все просто. Но делаем ли мы так, это же не совсем удобно? Как организовать удобство работы?

Можно везде захардкодить функцию, которая дает нам нужный компонент и мы получает плохо пахнущий глобальный синглтон. Теперь все зависит от того, позволяет ли наша платформа заменить определенную в основном коде функцию mockом. Позволяет - хорошо. Такой вариант все же не идеален, так как нет удобного способа увидеть зависимости компонента, им воспользоваться могут где угодно и как угодно. Вообще не понятно, какие компоненты используются в коде, какие из них надо заменить на mocks

Может кто-то просветит как пишут и тестируют большие приложения? Обычно для Haskell и подобных любая статья рассматривает какой-то примитивный замкнутый кусок программы или очень простую программу

vertexua ★★★☆☆ ()
Последнее исправление: vertexua (всего исправлений: 1)
Ответ на: комментарий от vertexua

позволяет ли наша платформа заменить определенную в основном коде функцию mockом

Такое только в лиспах хорошо делается, насколько я знаю. А хаскель тут не сильно от всяких жабок отличается, по-моему — надо всякие factory/dependency injection использовать, чтобы потом можно было mock-и подпихнуть.

pitekantrop ★★★ ()

А никак. И это еще цветочки, во всяких говнолиспах вообще нет раздельной компиляции и смысл функции или макроса зависит от всего контекста. Юнит-тестирование невозможно в принципе.

anonymous ()
Ответ на: комментарий от anonymous

А как ты сформируешь корректное окружение для вызова этих функций, и как создашь тестовые аргументы?

anonymous ()
Ответ на: комментарий от pitekantrop

в haskell с persistent для тестов можно использовать in-memory-storage вместо базы:

runResourceT $ withSqliteConn ":memory:" $ do {-your tests-}

и вообще не надо никаких factory/dependency injection и прочего..

qnikst ★★★★★ ()
Ответ на: комментарий от vertexua

Тем более как мы подключаем компонент? Если передаем как параметр в функции, то вопрос с mockом отпадает, все просто. Но делаем ли мы так, это же не совсем удобно? Как организовать удобство работы?

может логичнее рассматривать готовые библиотеки и смотреть, как это сделано в них? (про persistent я выше написал).

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

если нету параметра то можно иметь 2 модуля Database.Mypackage и Database.Mypackage.Mock с одинаковыми экспортируемыми функциями и в тестах грузить Database.Mypackage.Mock, или даже использовать как #ifdef TEST \n import Database.Mypackage.Mock #else ... Но большого смысла в этом нет, т.к. тесты в любом случае отделены от кода.

Может кто-то просветит как пишут и тестируют большие приложения? Обычно для Haskell и подобных любая статья рассматривает какой-то примитивный замкнутый кусок программы или очень простую программу

это конечно лучше в рассылке, а не на ЛОРе задавать, там ответов побольше будет, в haskell есть quickcheck (вроде smoke test это зовётся), где проверяются инварианты, которые должны сохраняться, плюс есть HUnit/HSpec (стандартное unit-тестирования) и инструменты для проверки покрытия кода тестами, в общем всё как обычно.

qnikst ★★★★★ ()
Ответ на: комментарий от qnikst

in-memory-storage вместо базы:

:facepalm:

А развить тему в сторону интеграционных и регрессионных тестов не желаешь? Терабайтные базы тоже в память заливать будем при тестировании?

d_Artagnan ★★ ()
Ответ на: комментарий от d_Artagnan

а с каких пор в Mock-объектами имитируются базы на гиги данных?

qnikst ★★★★★ ()
Ответ на: комментарий от dmitry_malikov

Как, например, смоделировать следующие события в формате теста ?: есть сервер, есть клиент; стартует сервер, стартует 10000 клиентов, клиенты срут командочкой в сервер, клиенты дохнут; проверить, что сервер остался живым.

Это не задача unit-тестирования. Это нагрузочное тестирование уже какое-то.

drakmail ★★★★ ()
Ответ на: комментарий от d_Artagnan

Хаскель не ЯОН

Так и есть. Хаскелль - исследовательский язык.

LongLiveUbuntu ★★★★★ ()
Ответ на: комментарий от d_Artagnan

Тут только бояться можно ошибок связанных с базой, которые обычно очень громкие чтобы из найти, например ошибка синтаксиса, отвалятся с грохотом на pre-production или таки в интеграционных. Для простых тестов логики база в памяти наоборот самое адекватное решение

vertexua ★★★☆☆ ()
Ответ на: комментарий от LongLiveUbuntu

Одно другому не мешает: исследовательский ЯП может как быть ЯОН, так и не быть.

d_Artagnan ★★ ()
Ответ на: комментарий от dmitry_malikov

Интеграционные тесты. Делаются в другом формате.

note173 ★★★★★ ()

Haskell + СУБД + сеть = /0

Зачем пытаться решать практические задачи на языке, не предназначенном для этого?

anonymous ()

Как обстоят дела с unit-тестированием в функциональных языках?

Если про Haskell — http://stackoverflow.com/q/3120796/1337941.

Интересуют в первую очередь «чистые» языки (Haskell).

Haskell не то чтобы «чистый», скорее «в том числе чистый», также как и «отчасти императивный» — просто «чистые», «грязные» и прочие уровни разделены. Но вещи с состоянием, настоящие замыкания, процессы и т.п. всё равно есть, так что понятиям «объект», «актор» и тому подобным можно придать смысл.

Вот в ОО-языках, например, есть такой подход

http://www.jamesshore.com/Blog/Dependency-Injection-Demystified.html

Соответственно

{-# LANGUAGE ScopedTypeVariables, ExistentialQuantification #-}

-- | For "RAII" and assertions.
import Control.Exception ( bracket, assert, evaluate )

-- | "Opaque" or DB independent data types.
data DatabaseSettings = SomeSettings
data DatabaseRequest = SomeRequest
data DatabaseReply = SomeReply deriving ( Show )
data DatabaseAssertion = SomeAssertion

-- | Typeclass, interface, trait, concept, pure abstract class, ...
class DatabaseInterface db where
  open :: DatabaseSettings -> IO db
  close :: db -> IO ()
  transaction :: db -> DatabaseRequest -> IO DatabaseReply
  assertion :: db -> DatabaseAssertion -> IO Bool

-- | Ad-hoc polymorphic (generic) function.
withDatabase :: DatabaseInterface db => DatabaseSettings -> (db -> IO a) -> IO a
withDatabase settings = bracket (open settings) close

-- | "Real" object.
data RealDB = RealDB

-- | Proper interface implementition.
instance DatabaseInterface RealDB where
  open _ = putStrLn "open RealDB" >> return RealDB
  close _ = putStrLn "close RealDB"
  transaction _ _ = putStrLn "do transaction on RealDB" >> return SomeReply
  assertion _ _ = error "won't do assertion on RealDB"

-- | "Class" = functions closed over data fileds + subtyping.
data Example = forall t. DatabaseInterface t => Example {
  _db      :: t,
           -- ^ Any instance of DatabaseInterface ("subtyping").
  _doStuff :: DatabaseRequest -> IO ()
           -- ^ Closure over _db :: DatabaseInterface t => t ("method").
}

-- | Handwaving application for upcasts.
instance DatabaseInterface Example where
  open _ = error "open on Example is too polymorphic (?)"
  close (Example db _) = close db
  transaction (Example db _) = transaction db
  assertion (Example db _) = assertion db

-- | (generic) "constructor" with dependency injection.
anyExample :: DatabaseInterface db => db -> IO Example
anyExample db = return Example { -- <- DI.
  _db = db,
  _doStuff = \req -> do -- <- Closure.
    rep <- transaction db req
    print rep
  }

-- | Simple "constructor" for a specific type.
realExample :: IO Example
realExample = do
  db :: RealDB <- open SomeSettings
  anyExample db

-- | (almost) simple function.
withReal :: (Example -> IO a) -> IO a
withReal = bracket realExample close

-- | Mock object.
data MockDB = MockDB

-- | Interface emulation.
instance DatabaseInterface MockDB where
  open _ = putStrLn "open MockDB" >> return MockDB
  close _ = putStrLn "close MockDB"
  transaction _ _ = putStrLn "do transaction on MockDB" >> return SomeReply
  assertion _ _ = putStrLn "do assertion on MockDB" >> return True

-- | Real and mock objects + assertion on mock ("unit test").
testDoStuff :: IO ()
testDoStuff = do
  -- mock
  withDatabase SomeSettings $
    \(mockDB :: MockDB) -> do 
      mockObject <- anyExample mockDB
      _doStuff mockObject SomeRequest
      assertIO =<< assertion mockObject SomeAssertion
  -- real
  withReal $
    \realObject -> do
      _doStuff realObject SomeRequest
      assertIO =<< assertion realObject SomeAssertion

assertIO :: Bool -> IO () 
assertIO cond = evaluate (assert cond ())

то есть и чистые структуры данных и объекты с состоянием делаются ADT (чистыми и вокруг ST/IO/STM (то есть они агрегируют разные MVar, TChan и т.п., а потом уже попадают в соответствующие монады)), интерфейсы — классами типов, связывание данных и функций — замыканиями и завязываниями узлов, элементы позднего связывания и IoC/DI — ExistentialQuantification.

Или вот проще пример:

module Logger where

import Prelude hiding ( log )
import System.IO hiding ( hPutStrLn )
import Control.Exception
import Data.ByteString.Char8

data Logger = Logger
  { _log :: Handle
  , _write :: ByteString -> IO ()
  }

makeLogger :: FilePath -> IO Logger
makeLogger logPath = do
  log <- openFile logPath WriteMode
  return Logger
    { _log = log
    , _write = \datum -> hPutStrLn log datum >> hFlush log
    }

cleanOnLogger :: Logger -> IO ()
cleanOnLogger = hClose . _log

withLogger :: FilePath -> (Logger -> IO a) -> IO a
withLogger logPath = bracket (makeLogger logPath) cleanOnLogger

если агрегировать:

import Logger

data Server = Server
  { _logger :: Logger -- "parrent"
  , _host :: HostPreference -- "const's"
  , _port :: Int
  , _app :: AppData IO -> IO () -- "method"
  TVar's, etc. -- "mutable's"
  }

то можно делать обычную аппликацию:

withLogger "/tmp/server.log" $ makeServer HostAny 8080 >=> runServer

завязанную на конкретный Logger.Logger. С переходом к

class LoggerI t where
  ...

data Server = forall t. LoggerI t => Server
  { _logger :: t
  ...
  }

можно уже выбирать разные инстансы (в том числе mock) LoggerI.

Ну и ещё можно в xmonad посмотреть - и про ExistentialQuantification и про QuickCheck.

quasimoto ★★★★ ()
Ответ на: комментарий от qnikst

А эта штука работает только со sql-бэкендами?

KblCb ★★★★★ ()
Ответ на: комментарий от KblCb

должна со всем с чем умеет persistent уметь, т.к. это просто обёртка над написанием raw-sql в нём.

qnikst ★★★★★ ()
Ответ на: комментарий от qnikst

Так в том-то и соль, в том-то и изюминка, что не все бекенды persistent умеют sql. Я правда такими не пользовался но…

KblCb ★★★★★ ()
Ответ на: комментарий от KblCb

если ты про NoSQL, то судя по документации ескулетто, оно их умеет.

qnikst ★★★★★ ()
Ответ на: комментарий от quasimoto

Как-то извращенно.

Но вот в эту же тему подойдёт: как сделать аналогичные моки для, например, работы с сетью, вот есть куча данных из Network.ByteString без тайпклассов.

Я хочу сделать так, чтобы recvFrom выдавало мне данные из списка [(Int, (ByteString, sockAddr))], которые я в тестовом файле могу заполнить. В итоге я встаю перед выбором или делать так, чтобы мой модуль содержал class где будут задаваться все команды и по умолчанию заполнять их рабочими, а в тестах переобределять на варианты из Mock, но это слегка некрасиво.

Или извращаться и идти дальше делать модуль с Mock, например, Network.ByteString.Mock и в коде делать:

#ifdef TEST
include Network.ByteString.Mock
#else
include Network.ByteString

В этом случае для тестов и для production будет собран немного разный модуль, но уже можно управлять тестами.

qnikst ★★★★★ ()
Ответ на: комментарий от qnikst

Как-то извращенно.

Элементы IoC/DI и свободной конфигурации в хаскеле, например http://www.haskell.org/haskellwiki/Xmonad/Guided_tour_of_the_xmonad_source/Co..., разве что оно там не для моков, а для гибкой конфигурации - но моки подсовываются по тому же принципу.

как сделать аналогичные моки для, например, работы с сетью, вот есть куча данных из Network.ByteString без тайпклассов.

Никак, там же простофункции, то есть такое поведение не предусмотрено.

quasimoto ★★★★ ()
Ответ на: комментарий от quasimoto

Никак, там же простофункции, то есть такое поведение не предусмотрено.

но именно это и интересно..

qnikst ★★★★★ ()
Ответ на: комментарий от qnikst

Такое в Java может быть просто (обычных функций считай нет и всюду объекты с состоянием да виртуальные интерфейсы), а вот в плюсах мокабельность это тоже константное проседание производительности и design choice, её нужно специально готовить (как с gmock), то есть просто так для любого класса она не получится. Для простых функций, особенно чистых и вообще stateless - тем более, если только везде намеренно, опять-таки, использовать указатели на функции а не сами функции (и, опять, будет гибче, но «медленнее»).

Для примитивных IO функций ничего лучше внесения условной компиляции которая будет оставлять от реальной IO функции её имитацию для использования в unit тестах я не вижу.

quasimoto ★★★★ ()
Ответ на: комментарий от quasimoto

Ну и по аналогии с указателем на функцию можно помещать настоящую recvFrom и её имитации в IORef/TVar и везде использовать этот ref/var:

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, FlexibleInstances #-}

import Data.IORef
import System.IO.Unsafe
import Control.Applicative

import Data.ByteString
import Network.Socket hiding ( recvFrom )
import qualified Network.Socket.ByteString as N ( recvFrom )

data RecvFrom = RecvFrom {
  _pointer :: IORef (Socket -> Int -> IO (ByteString, SockAddr))
}

recvFrom :: RecvFrom
recvFrom = unsafePerformIO $ RecvFrom <$> newIORef N.recvFrom 
{-# NOINLINE recvFrom #-}

-- Ya heard 'bout scala?
class Function t r | t -> r where
  apply :: t -> r

instance Function RecvFrom (Socket -> Int -> IO (ByteString, SockAddr)) where
  apply (RecvFrom ref) sock num = readIORef ref >>= \f -> f sock num

mockRecvFrom :: Socket -> Int -> IO (ByteString, SockAddr)
mockRecvFrom _ _ = ...

...

  apply recvFrom socket num -- real recvFrom from Network.Socket.ByteString
  writeIORef (_pointer recvFrom) mockRecvFrom -- change it to mockRecvFrom
  apply recvFrom undefined undefined -- use mock
quasimoto ★★★★ ()
Последнее исправление: quasimoto (всего исправлений: 1)
Ответ на: комментарий от qnikst

runResourceT

Это что-то вроде ReaderT?

То есть, получается, можно делать

class Injection inj where
  -- inj interface

-- different instances

type DI inj a = ReaderT inj IO a

someAction :: Injection inj => DI inj ()
someAction = -- ask / liftIO / use inj interface

withInjection :: Injection inj => inj -> DI inj a -> IO a
withInjection = flip runReaderT

test :: IO ()
test = withInjection someMock someAction

main :: IO ()
main = withInjection realThing someAction -- the same action

например

class Connection conn where
  -- DB connection interface

-- different DB drivers

type DB conn a = ReaderT conn IO a

someQuery :: Connection conn => DB conn ()
someQuery = -- ask / liftIO / use DB conn interface

withConnection :: Connection conn => conn -> DB conn a -> IO a
withConnection = flip runReaderT

test :: IO ()
test = withConnection mockDB someQuery

main :: IO
main = withConnection realDB someQuery

в крайнем случае - завернуть inj/conn в DI/DB с ExistentialQuantification.

Разве что обычные статические/чистые функции остаются незаменимы, но они везде такие.

quasimoto ★★★★ ()
Ответ на: комментарий от quasimoto

Это что-то вроде ReaderT?

вроде, если точнее, то это недорегионы.

В принципе с таким подходом :memory: это уже мок, достаточный для тестов и дальше ничего изобретать не нужно, но подобный интерфейс есть не везде.

qnikst ★★★★★ ()
Ответ на: комментарий от vertexua

Может кто-то просветит как пишут и тестируют большие приложения? Обычно для Haskell и подобных любая статья рассматривает какой-то примитивный замкнутый кусок программы или очень простую программу
пишут и тестируют большие приложения
Haskell
большие приложения
Haskell

Лiлчто?

anonymous ()
Ответ на: комментарий от anonymous

А никак. И это еще цветочки, во всяких говнолиспах вообще нет раздельной компиляции

Это ты с чего взял? Все есть. Ну, в нормальных лиспах.

Юнит-тестирование невозможно в принципе.

наличие библиотек для юнит-тестирование полностью опровергает этот тезис.

ЗЫ: а вообще молодец, вброс засчитан

anonymous ()
Ответ на: комментарий от dmitry_malikov

Но в случае с Хаскелем это проверить не получится. Деньги за написание такой программы никто не заплатит, а писать что-то, где «есть сервер, есть клиент; стартует сервер, стартует 10000 клиентов» для души... Ну я надеюсь, что человека с такой мелкой и черной душонкой здесь нет.

anonymous ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.