Specflow is a fantastic framework for codifying your requirements. It allows writing human-readable scenarios which map to C# code. This, coupled with a browser automation tool like playwright, puppeteer, or selenium can create an incredibly powerful and versatile testing framework.
In this article, I’ll show you how to set up a basic test framework for the default MVC project (though the underlying principles apply to any .NET project really).
As a starting point, I've created a very simple MVC application (the default one from VS2019). It had the following structure:
And looked like this:
Then we'll need to setup a test project, that I have called TestSpecflowApplication.Test
:
You will need the following NuGet packages installed in your project:
1 2 3 4 5 |
Microsoft.NET.Test.Sdk NUnit NUnit3TestAdapter SpecFlow.NUnit Microsoft.Playwright.NUnit |
For our testing, we'll need 3 files, the feature file with the SpecFlow test, the SpecFlow implementation behind each step, and the Kestrel server setup.
For our SpecFlow feature called HomeScreen.feature
, will check for the Welcome header on the home page:
1 2 3 4 5 6 |
Feature: Home Screen Scenario: Client lands on the home screen and sees the welcome message Given I am a visitor When I enter the home page Then I should see the 'Welcome' header |
The next step is to tie the scenario steps to step definitions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
[Binding] public class ExampleSteps { private IPage _page; [Given(@"I am a visitor")] public void GivenIAmAVisitor() { // left empty } [When(@"I enter the home page")] public async Task WhenIEnterTheHomePage() { var playwright = await Playwright.CreateAsync(); var browser = await playwright.Chromium.LaunchAsync(); _page = await browser.NewPageAsync(); await _page.GotoAsync("https://localhost:5001/"); } [Then(@"I should see the '(.*)' header")] public async Task ThenIShouldSeeTheHeader(string expectedHeader) { var header = await _page.QuerySelectorAsync("h1"); Assert.AreEqual(expectedHeader, await header.InnerTextAsync()); } } |
Finally, before we run any tests, we need to make sure Kestrel server is up and running:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
[Binding] public class ExampleSteps { private IPage _page; [Given(@"I am a visitor")] public void GivenIAmAVisitor() { // left empty } [When(@"I enter the home page")] public async Task WhenIEnterTheHomePage() { var playwright = await Playwright.CreateAsync(); var browser = await playwright.Chromium.LaunchAsync(); _page = await browser.NewPageAsync(); await _page.GotoAsync("https://localhost:5001/"); } [Then(@"I should see the '(.*)' header")] public async Task ThenIShouldSeeTheHeader(string expectedHeader) { var header = await _page.QuerySelectorAsync("h1"); Assert.AreEqual(expectedHeader, await header.InnerTextAsync()); } } |
Fingers Crossed:
Conclusion
This is obviously a very simple example. The handling of Playwright was a little bit hacked, perhaps use it with BoDi dependency injection.
In this code, we also don't have a database. In memory DB could be an interesting use case here, but perhaps setting up a local instance of SQL db could go a long way, could be deleted afterwards.
The benefit of running the tests also could be docerized and don't interferrer with any production code.
There is a also the question of the environment (you want to run your tests against prod environment right?).