Using F# for testing
https://swlaschin.gitbooks.io/fsharpforfunandprofit/content/posts/low-risk-ways-to-use-fsharp-at-work-3.html
如果您想在不接触核心代码的情况下开始使用 F# 编写有用的代码,那么编写测试是一个很好的开始方式。
F# 不仅具有更紧凑的语法,而且还具有许多不错的功能,例如“双反引号”语法,可使测试名称更具可读性。
与本系列中的所有建议一样,我认为这是一个低风险的选择。测试方法往往很简短,因此几乎任何人都可以阅读它们而无需深入了解 F#。在最坏的情况下,您可以轻松地将它们移植回 C#。
10. 使用 F# 编写具有可读名称的单元测试
The code for this section is available on github.
就像 C# 一样,F# 可用于使用 NUnit、MsUnit、xUnit 等标准框架编写标准单元测试。
下面是为与 NUnit 一起使用而编写的测试类的示例。
[<TestFixture>]
type TestClass() =
[<Test>]
member this.When2IsAddedTo2Expect4() =
Assert.AreEqual(4, 2+2)如您所见,有一个具有 TestFixture 属性的类和一个具有 Test 属性的 public void 方法。都非常标准。
但是,当您使用 F# 而不是 C# 时,您会得到一些不错的附加功能。首先,您可以使用双反引号语法来创建更具可读性的名称,其次,您可以在模块而不是类中使用 let 绑定函数,从而简化代码。
[<Test>]
let ``When 2 is added to 2 expect 4``() =
Assert.AreEqual(4, 2+2)双反引号语法使测试结果更易于阅读。这是具有标准类名的测试输出:
TestClass.When2IsAddedTo2Expect4
Result: Success与使用更友好名称的输出的对比:
MyUnitTests.When 2 is added to 2 expect 4
Result: Success因此,如果您想编写非程序员也可以看懂的测试名称,请试试 F#!
11. 使用 F# 以编程方式运行单元测试
通常,你可能想以编程方式运行单元测试。这可能是出于各种原因,如使用自定义过滤器,或做自定义日志,或不想在测试机器上安装NUnit。
一种简单的方法是使用 Fuchu库(译:停止更新于Nov 24, 2020,我看到这里有点不想继续翻了,这都什么啊,啥啥都是过期的,啥啥都是无人更新的,啥啥都是过期的),它可以让您直接组织测试,尤其是参数化测试,而无需任何复杂的测试属性。
这是一个例子:
let add1 x = x + 1
// a simple test using any assertion framework:
// Fuchu's own, Nunit, FsUnit, etc
let ``Assert that add1 is x+1`` x _notUsed =
NUnit.Framework.Assert.AreEqual(x+1, add1 x)
// a single test case with one value
let simpleTest =
testCase "Test with 42" <|
``Assert that add1 is x+1`` 42
// a parameterized test case with one param
let parameterizedTest i =
testCase (sprintf "Test with %i" i) <|
``Assert that add1 is x+1`` i您可以使用如下代码直接在 F# 交互式中运行这些测试:run simpleTest
您还可以将这些测试组合成一个或多个列表,或列表的分层列表:
// create a hierarchy of tests
// mark it as the start point with the "Tests" attribute
[<Fuchu.Tests>]
let tests =
testList "Test group A" [
simpleTest
testList "Parameterized 1..10" ([1..10] |> List.map parameterizedTest)
testList "Parameterized 11..20" ([11..20] |> List.map parameterizedTest)
]The code above is available on github.
最后,有了 Fuchu,测试组件就变成了自己的测试运行器。只需将程序集设为控制台应用程序而不是库并将此代码添加到 program.fs 文件:
[<EntryPoint>]
let main args =
let exitCode = defaultMainThisAssembly args
Console.WriteLine("Press any key")
Console.ReadLine() |> ignore
// return the exit code
exitCodeUsing the NUnit test runner
如果您确实需要使用现有的测试运行器(例如 NUnit 测试运行器),那么将一个简单的脚本放在一起来执行此操作非常简单。
我在下面使用 Nunit.Runners 包做了一个小例子。
好吧,这可能不是最令人兴奋的 F# 用法,但它确实展示了 F# 的“对象表达式”语法来创建 NUnit.Core.EventListener 接口,所以我想我应该把它作为演示。
// sets the current directory to be same as the script directory
System.IO.Directory.SetCurrentDirectory (__SOURCE_DIRECTORY__)
// Requires Nunit.Runners under script directory
// nuget install NUnit.Runners -o Packages -ExcludeVersion
#r @"Packages\NUnit.Runners\tools\lib\nunit.core.dll"
#r @"Packages\NUnit.Runners\tools\lib\nunit.core.interfaces.dll"
open System
open NUnit.Core
module Setup =
open System.Reflection
open NUnit.Core
open System.Diagnostics.Tracing
let configureTestRunner path (runner:TestRunner) =
let package = TestPackage("MyPackage")
package.Assemblies.Add(path) |> ignore
runner.Load(package) |> ignore
let createListener logger =
let replaceNewline (s:string) =
s.Replace(Environment.NewLine, "")
// This is an example of F#'s "object expression" syntax.
// You don't need to create a class to implement an interface
{new NUnit.Core.EventListener
with
member this.RunStarted(name:string, testCount:int) =
logger "Run started "
member this.RunFinished(result:TestResult ) =
logger ""
logger "-------------------------------"
result.ResultState
|> sprintf "Overall result: %O"
|> logger
member this.RunFinished(ex:Exception) =
ex.StackTrace
|> replaceNewline
|> sprintf "Exception occurred: %s"
|> logger
member this.SuiteFinished(result:TestResult) = ()
member this.SuiteStarted(testName:TestName) = ()
member this.TestFinished(result:TestResult)=
result.ResultState
|> sprintf "Result: %O"
|> logger
member this.TestOutput(testOutput:TestOutput) =
testOutput.Text
|> replaceNewline
|> logger
member this.TestStarted(testName:TestName) =
logger ""
testName.FullName
|> replaceNewline
|> logger
member this.UnhandledException(ex:Exception) =
ex.StackTrace
|> replaceNewline
|> sprintf "Unhandled exception occurred: %s"
|> logger
}
// run all the tests in the DLL
do
let dllPath = @".\bin\MyUnitTests.dll"
CoreExtensions.Host.InitializeService();
use runner = new NUnit.Core.SimpleTestRunner()
Setup.configureTestRunner dllPath runner
let logger = printfn "%s"
let listener = Setup.createListener logger
let result = runner.Run(listener, TestFilter.Empty, true, LoggingThreshold.All)
// if running from the command line, wait for user input
Console.ReadLine() |> ignore
// if running from the interactive session, reset session before recompiling MyUnitTests.dllThe code above is available on github.
12. Use F# to learn to write unit tests in other ways
上面的单元测试代码我们都很熟悉,但是还有其他的方式来编写测试。学习以不同的风格编写代码是将一些新技术添加到您的技能库和扩展您的一般思维的好方法,所以让我们快速浏览一下其中的一些。
首先是 FsUnit,它用更流畅和惯用的方法(自然语言和管道)取代了 Assert。
这是一个片段:
open NUnit.Framework
open FsUnit
let inline add x y = x + y
[<Test>]
let ``When 2 is added to 2 expect 4``() =
add 2 2 |> should equal 4
[<Test>]
let ``When 2.0 is added to 2.0 expect 4.01``() =
add 2.0 2.0 |> should (equalWithin 0.1) 4.01
[<Test>]
let ``When ToLower(), expect lowercase letters``() =
"FSHARP".ToLower() |> should startWith "fs"The above code is available on github.
Unquote使用的是一种非常不同的方法。Unquote的方法是将任何F#表达式包裹在F#引号中,然后对其进行评估。如果一个测试表达式抛出了一个异常,测试就会失败,并且不仅打印出这个异常,还打印出到异常发生时的每一步。这些信息有可能让你对断言失败的原因有更多的了解。(译:这个库也停更了)
这是一个非常简单的例子:
[<Test>]
let ``When 2 is added to 2 expect 4``() =
test <@ 2 + 2 = 4 @>还有一些快捷运算符,例如 =?和 >?这使您可以更简单地编写测试 - 没有任何地方断言!
[<Test>]
let ``2 + 2 is 4``() =
let result = 2 + 2
result =? 4
[<Test>]
let ``2 + 2 is bigger than 5``() =
let result = 2 + 2
result >? 5The above code is available on github.
13. Use FsCheck to write better unit tests(该库最后一次更新在Nov 7, 2022,不翻译该部分)
14. Use FsCheck to create random dummy data(该库最后一次更新在Nov 7, 2022,不翻译该部分)
15. Use F# to create mocks
如果您使用 F# 为用 C# 编写的代码编写测试用例,您可能希望为接口创建模拟和存根。
In C# you might use Moq or NSubstitute. In F# you can use object expressions to create interfaces directly, or the Foq library.
在 C# 中,您可以使用 Moq 或 NSubstitute。在 F# 中,您可以使用对象表达式直接创建接口,也可以使用 Foq 库。(也停更了Jun 29, 2018)
两者都很容易用,而且方式类似于Moq.
这是 C# 中的一些 Moq 代码:
// Moq Method
var mock = new Mock<IFoo>();
mock.Setup(foo => foo.DoSomething("ping")).Returns(true);
var instance = mock.Object;
// Moq Matching Arguments:
mock.Setup(foo => foo.DoSomething(It.IsAny<string>())).Returns(true);
// Moq Property
mock.Setup(foo => foo.Name ).Returns("bar");下面是 F# 中等效的 Foq 代码:
// Foq Method
let mock =
Mock<IFoo>()
.Setup(fun foo -> <@ foo.DoSomething("ping") @>).Returns(true)
.Create()
// Foq Matching Arguments
mock.Setup(fun foo -> <@ foo.DoSomething(any()) @>).Returns(true)
// Foq Property
mock.Setup(fun foo -> <@ foo.Name @>).Returns("bar")有关 F# 中使用 Mock 的更多信息,请参阅:
And you need to mock external services such as SMTP over the wire, there is an interesting tool called mountebank, which is easy to interact with in F#.
16. Use F# to do automated browser testing(TODO:未来再翻,这段好无聊)
In addition to unit tests, you should be doing some kind of automated web testing, driving the browser with Selenium or WatiN.
But what language should you write the automation in? Ruby? Python? C#? I think you know the answer!
To make your life even easier, try using Canopy, a web testing framework built on top of Selenium and written in F#. Their site claims "Quick to learn. Even if you've never done UI Automation, and don't know F#.", and I'm inclined to believe them.
Below is a snippet taken from the Canopy site. As you can see, the code is simple and easy to understand.
Also, FAKE integrates with Canopy, so you can run automated browser tests as part of a CI build.
//start an instance of the firefox browser
start firefox
//this is how you define a test
"taking canopy for a spin" &&& fun _ ->
//go to url
url "http://lefthandedgoat.github.io/canopy/testpages/"
//assert that the element with an id of 'welcome' has
//the text 'Welcome'
"#welcome" == "Welcome"
//assert that the element with an id of 'firstName' has the value 'John'
"#firstName" == "John"
//change the value of element with
//an id of 'firstName' to 'Something Else'
"#firstName" << "Something Else"
//verify another element's value, click a button,
//verify the element is updated
"#button_clicked" == "button not clicked"
click "#button"
"#button_clicked" == "button clicked"
//run all tests
run()17. Use F# for Behaviour Driven Development(TODO:未来再翻,这段也好无聊,说实话我在现在的网上就没见过讨论BDD的,能给点案例么)
The code for this section is available on github.
If you're not familiar with Behaviour Driven Development (BDD), the idea is that you express requirements in a way that is both human-readable and executable.
The standard format (Gherkin) for writing these tests uses the Given/When/Then syntax -- here's an example:
Feature: Refunded or replaced items should be returned to stock
Scenario 1: Refunded items should be returned to stock
Given a customer buys a black jumper
And I have 3 black jumpers left in stock
When they return the jumper for a refund
Then I should have 4 black jumpers in stockIf you are using BDD already with .NET, you're probably using SpecFlow or similar.
You should consider using TickSpec instead because, as with all things F#, the syntax is much more lightweight.
For example, here's the full implementation of the scenario above.
type StockItem = { Count : int }
let mutable stockItem = { Count = 0 }
let [<Given>] ``a customer buys a black jumper`` () =
()
let [<Given>] ``I have (.*) black jumpers left in stock`` (n:int) =
stockItem <- { stockItem with Count = n }
let [<When>] ``they return the jumper for a refund`` () =
stockItem <- { stockItem with Count = stockItem.Count + 1 }
let [<Then>] ``I should have (.*) black jumpers in stock`` (n:int) =
let passed = (stockItem.Count = n)
Assert.True(passed)The C# equivalent has a lot more clutter, and the lack of double backtick syntax really hurts:
[Given(@"a customer buys a black jumper")]
public void GivenACustomerBuysABlackJumper()
{
// code
}
[Given(@"I have (.*) black jumpers left in stock")]
public void GivenIHaveNBlackJumpersLeftInStock(int n)
{
// code
}Examples taken from the TickSpec site.
Summary of testing in F#
您当然可以结合我们迄今为止所见的所有测试技术(as this slide deck demonstrates):
单元测试(FsUnit、Unquote)和基于属性的测试(FsCheck)。【停更一堆】
由浏览器自动化 (Canopy) 驱动的用 BDD (TickSpec) 编写的自动化验收测试(或至少是冒烟测试)。
两种类型的测试都在每个构建上运行(使用 FAKE)。
那里有很多关于测试自动化的建议,您会发现很容易将概念从其他语言移植到这些 F# 工具。玩得开心!
Last updated