Тестирование в F#

    Введение


    Вы наверное уже слышали много хорошего о языке F#, и даже наверное успели его опробовать на небольших личных проектах. Но как быть если речь идет о чем-то немного большем чем просто запуск и отладка простого консольного приложения или скрипта? В этой статье я поведаю вам о моем личном опыте работы с тестами в F#. Кому интересно, прошу в подкат.



    Исходный код


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


    [<AutoOpen>]
    module DistanceUnits
    
    open System
    
    [<Measure>] type m
    [<Measure>] type cm
    [<Measure>] type inch
    [<Measure>] type ft
    [<Measure>] type h
    
    let mPerCm : float<m/cm> = 0.01<m/cm>
    let cmPerInch : float<cm/inch> = 2.54<cm/inch>
    let inchPerFeet: float<inch/ft> = 12.0<inch/ft>
    
    let metersToCentimeters (x: float<m>) = x / mPerCm
    let centimetersToInches (x: float<cm>) = x / cmPerInch
    let inchesToFeets (x:float<inch>) = x / inchPerFeet
    
    let centimetersToMeters: float<cm> -> float<m> = ( * ) mPerCm
    let inchesToCentimeters: float<inch> -> float<cm> = ( * ) cmPerInch
    let metersToInches: float<m> -> float<inch> = metersToCentimeters >> centimetersToInches
    let metersToFeets: float<m> -> float<ft> =  metersToInches >> inchesToFeets
    let feetsToInches: float<ft> -> float<inch> = ( * ) inchPerFeet
    let metersToHours(m: float<m>): int<h> = raise(new InvalidOperationException("Unsupported operation"))

    Библиотека для тестирования


    В принципе для тестирования ваших приложений на F# вы можете обойтись без каких либо специальных библиотек. Хотя если вы, как и я, предпочитаете более стандартный подход, то вы без проблем сможете воспользоваться такими библиотеками как:



    Здесь я не буду вдаваться в детали типа какой фреймворк самый лучший в мире, оставлю это на ваше усмотрение. Я отдаю предпочтение xUnit и далее буду использовать его, если ваши предпочтения не совпадают с моими то вы лего можете переключиться на вашу любимую библиотеку для тестирования.


    Итак для начала добавьте в ваш проект пакеты xunit и xunit.runner.visualstudio



    Assert библиотеки


    Каждый маломальский тестовый фреймворк предоставляет вам минимальный набор assert-функций. В принципе их хватает в 90% случаев, но ими не совсем удобно пользоваться. Давайте рассмотрим парочку дополнительных и удобных библиотек.


    • Fluent Assertions — Интересное решение для любителей цепочных вызовов. Неплохо работает в C#, но к сожалению неуклюже в F#, так как цепочка никогда не возвращает Unit, следовательно вам нужно всегда изворачиваться и писать что-то вроде actual.Should().StartWith("S") |> ignore.
    • FsUnit — Библиотека написанная специально для F#, но заточенная изначально под NUnit. С примерами вы можете ознакомиться здесь. Имеет в наличие поддержку xUnit, но поддержка это выглядит ограниченно и поддерживается слабовато, а жаль.
    • Unquote — Довольно интересное решение, использующее Quoted Expressions. Единственное, на мой взгляд, ограничение заключается в зависимости от F# версии 4.0 и выше. Далее в этой статье я буду использовать именно эту библиотеку.

    Mock библиотеки


    Если вы сталкиваетесь с необходимостью использовать Mock-и для тестирования вы можете воспользоваться Moq, но если вы ищите немного более F#-дружественного решения, вы можете воспользоваться Foq. Давайте сравним в использовании эти две библиотеки.

    Вызов метода в Moq:


    var mock = new Mock<IFoo>();
    mock.Setup(foo => foo.DoIt(1)).Returns(true);
    var instance = mock.Object;

    Вызов метода в Foq:


    let foo = Mock<IFoo>()
                .Setup(fun foo -> <@ foo.DoIt(1) @>).Returns(true)
                .Create()

    Сравнение аргументов в Moq:


    mock.Setup(foo => foo.DoIt(It.IsAny<int>())).Returns(true);

    Сравнение аргументов в Foq:


    mock.Setup(fun foo -> <@ foo.DoIt(any()) @>).Returns(true)

    Свойство в Moq:


    mock.Setup(foo => foo.Name ).Returns("bar");

    Свойство в Foq:


    mock.Setup(fun foo -> <@ foo.Name @>).Returns("bar")

    Другие полезности


    В зависимости от ваших нужд, вы так же можете воспользоваться известным "минимизатором Arrange фазы тестирования" и генератором заглушек — AutoFixture. Так же вы можете воспользоваться другими полезностями интеграции AutoFixture с xUnit.


    Написание тестов


    Итак, когда все готово, можно перейти к написанию тестов. xUnit позволяет нам использовать как стандартные классы так и определение модулей в F#, вам решать какой подход вам больше подходит. Ниже представлены примеры двух подходов.


    Класс:


    type ConverterTest1() =
        [<Fact>]
        member me.``It should convert meters to centimeters as expected``() =
    
            let actual = 1100.0<cm> |> centimetersToMeters
    
            test <@ actual = 11.0<m> @>
    
        [<Fact>]
        member me.``It should convert centimeters to meters as expected``() =
            let actual = 20.0<m> |> metersToCentimeters
    
            test <@ actual = 2000.00<cm> @>

    Модуль:


    module ConverterTest2 =
        open System
        [<Fact>]
        let ``It should convert meters to feets as expected`` () =
            let actual =  32.0<m> |> metersToFeets
    
            test <@ actual = 104.98687664041995<ft> @>
    
        [<Fact>]
        let ``It should fail when rubbish conversion is attempted`` () =
            raises<InvalidOperationException> <@ metersToHours 2.0<m> @

    Вместо заключения


    Выше приведенные тесты благополучно запускаются в студии и на интеграционном сервере. Благодарю за внимание. Надеюсь вам эта статья была полезной.

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

    Нужны ли unit-тесты, в их классическом понимании, в функциональных языках программирования

    Пишите ли вы тесты когда работаете с функциональными языками программирования

    • +12
    • 4,8k
    • 9
    Поделиться публикацией

    Комментарии 9

      +2
      Спасибо за статью!
      Даешь функциональное программирование в массы!
        0
        Пожалуйста, всегда рад стараться благой цели.
        0
        Тесты в классе так же можно определять с помощью «let».
          0
          Вы уверены? Ведь согласно документации:
          A let binding in a class type defines private fields and members for that class type...

          Может я вас не так понял или xUnit способен работать со скрытыми членами класса.
            0
            Да, именно. xUnit умеет работать с приватными методами.
              0
              Интересно, спасибо. Так можно избавиться от указателя this/me перед методом.
          +1
          Я бы для F# ещё написал бы про FsCheck (https://fscheck.github.io/FsCheck/) — феерически крутая штука.
            0
            Да согласен с вами, она тем паче, интереснее что сделанна как раз для неизменямой функциональной среды где f(x), не важно сколько раз, получившая, скажем, x=5, всегда возвращаяет один и тот же результат.
            0
            Спасибо за полезную статью! Надеюсь она попадет в F# weekly

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

            Самое читаемое