---
title: Appium Mobile Testing with Reqnroll and C# | TestingBot
description: Run BDD Appium mobile app tests with Reqnroll and C# on the TestingBot
  cloud. The community-maintained successor to SpecFlow for Gherkin scenarios against
  iOS simulators, Android emulators and real iPhones, iPads and Android devices.
source_url:
  html: https://testingbot.com/support/app-automate/appium/csharp/reqnroll
  md: https://testingbot.com/support/app-automate/appium/csharp/reqnroll/index.md
---
# Reqnroll Automated App Testing

[Reqnroll](https://reqnroll.net/) is an open-source .NET test automation framework for Behavior-Driven Development (BDD). It is the community-maintained successor to SpecFlow and is fully compatible with Gherkin syntax.

Reqnroll works with all major .NET versions (.NET 6.0, 7.0, 8.0, 9.0) and integrates with popular test frameworks:

- NUnit
- xUnit
- MSTest
- TUnit

**Migrating from SpecFlow?** Reqnroll is based on the SpecFlow codebase and provides an easy migration path. See the [migration section](https://testingbot.com#migration) below.

In the example below, we run a Gherkin scenario against the TestingBot demo app on both Android and iOS. The app adds two numbers and shows the sum.

## Installation

First, install the **Reqnroll for Visual Studio 2022** extension from the Visual Studio Marketplace to get syntax highlighting, navigation and project templates.

### Create a new project

Create a new test project and add the required packages:

    # Create a new NUnit test project
    dotnet new nunit -n AppiumReqnrollTests
    cd AppiumReqnrollTests
    
    # Add Reqnroll with NUnit integration
    dotnet add package Reqnroll.NUnit
    
    # Add the Appium .NET client
    dotnet add package Appium.WebDriver
    dotnet add package Selenium.WebDriver
    dotnet add package Selenium.Support

Or add these packages to your `.csproj` file:

    <ItemGroup>
      <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
      <PackageReference Include="NUnit" Version="4.1.0" />
      <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
      <PackageReference Include="Reqnroll.NUnit" Version="2.3.0" />
      <PackageReference Include="Appium.WebDriver" Version="6.1.0" />
      <PackageReference Include="Selenium.WebDriver" Version="4.27.0" />
      <PackageReference Include="Selenium.Support" Version="4.27.0" />
    </ItemGroup>

Alternative packages for other test frameworks:

- `Reqnroll.xUnit` - for xUnit 2.x
- `Reqnroll.xUnit.v3` - for xUnit 3.x
- `Reqnroll.MsTest` - for MSTest
- `Reqnroll.TUnit` - for TUnit

## Your first Reqnroll test

Reqnroll tests consist of three parts: a Feature file (Gherkin), Step Definitions (C#), and a Driver helper class.

### 1. Feature File

Create a `Features` folder and add `Calculator.feature`:

    Feature: Calculator
    
    Scenario Outline: Can do simple math
        Given I am using the calculator
        When I add inputA for "5"
        When I add inputB for "10"
        Then I should see the sum "15"

### 2. Driver Helper Class

Create a helper class to manage the Appium connection to TestingBot:

    using OpenQA.Selenium;
    using OpenQA.Selenium.Appium;
    using OpenQA.Selenium.Appium.Android;
    
    namespace AppiumReqnrollTests.Support;
    
    public class TestingBotDriver : IDisposable
    {
        public AndroidDriver Driver { get; private set; }
        private bool _testPassed = true;
    
        public void Initialize()
        {
            var options = new AppiumOptions
            {
                PlatformName = "Android",
                AutomationName = "UiAutomator2",
                App = "https://testingbot.com/appium/sample.apk"
            };
            options.AddAdditionalAppiumOption("appium:deviceName", "Galaxy S24");
            options.AddAdditionalAppiumOption("appium:platformVersion", "14.0");
            options.AddAdditionalAppiumOption("tb:options", new Dictionary<string, object>
            {
                ["key"] = Environment.GetEnvironmentVariable("TB_KEY") ?? "",
                ["secret"] = Environment.GetEnvironmentVariable("TB_SECRET") ?? "",
                ["name"] = "Reqnroll Appium Test",
                ["realDevice"] = true
            });
    
            Driver = new AndroidDriver(
                new Uri("https://hub.testingbot.com/wd/hub"),
                options,
                TimeSpan.FromSeconds(120)
            );
        }
    
        public void MarkTestFailed() => _testPassed = false;
    
        public void Dispose()
        {
            if (Driver != null)
            {
                var status = _testPassed ? "passed" : "failed";
                ((IJavaScriptExecutor)Driver).ExecuteScript($"tb:test-result={status}");
                Driver.Dispose();
            }
        }
    }

### 3. Step Definitions

Create `StepDefinitions/CalculatorSteps.cs`:

    using OpenQA.Selenium.Appium;
    using Reqnroll;
    using AppiumReqnrollTests.Support;
    
    namespace AppiumReqnrollTests.StepDefinitions;
    
    [Binding]
    public class CalculatorSteps
    {
        private readonly TestingBotDriver _tbDriver;
    
        public CalculatorSteps(TestingBotDriver tbDriver)
        {
            _tbDriver = tbDriver;
        }
    
        [Given("I am using the calculator")]
        public void GivenIAmUsingTheCalculator()
        {
            if (_tbDriver.Driver == null)
                _tbDriver.Initialize();
        }
    
        [When(@"I add inputA for ""(.*)""")]
        public void WhenIAddInputA(string amount)
        {
            var inputA = _tbDriver.Driver.FindElement(AppiumBy.AccessibilityId("inputA"));
            inputA.SendKeys(amount);
        }
    
        [When(@"I add inputB for ""(.*)""")]
        public void WhenIAddInputB(string amount)
        {
            var inputB = _tbDriver.Driver.FindElement(AppiumBy.AccessibilityId("inputB"));
            inputB.SendKeys(amount);
        }
    
        [Then(@"I should see the sum ""(.*)""")]
        public void ThenIShouldSeeTheSum(string sum)
        {
            var sumElement = _tbDriver.Driver.FindElement(AppiumBy.AccessibilityId("sum"));
            Assert.That(sumElement.Text, Is.EqualTo(sum));
        }
    }

### 4. Hooks for Setup/Teardown

Create `Hooks/TestHooks.cs` to manage the driver lifecycle:

    using Reqnroll;
    using AppiumReqnrollTests.Support;
    
    namespace AppiumReqnrollTests.Hooks;
    
    [Binding]
    public class TestHooks
    {
        private readonly TestingBotDriver _tbDriver;
    
        public TestHooks(TestingBotDriver tbDriver)
        {
            _tbDriver = tbDriver;
        }
    
        [AfterScenario]
        public void AfterScenario(ScenarioContext scenarioContext)
        {
            if (scenarioContext.TestError != null)
            {
                _tbDriver.MarkTestFailed();
            }
            _tbDriver.Dispose();
        }
    }

Run your tests with:

    TB_KEY=your_key TB_SECRET=your_secret dotnet test

## Real Device Testing
[Android](https://testingbot.com#)[iOS](https://testingbot.com#)

The example below runs the Reqnroll scenario on a real Android device. Set `realDevice: true` inside `tb:options` to target physical hardware.

    using OpenQA.Selenium;
    using OpenQA.Selenium.Appium;
    using OpenQA.Selenium.Appium.Android;
    
    namespace AppiumReqnrollTests.Support;
    
    public class TestingBotDriver : IDisposable
    {
        public AndroidDriver Driver { get; private set; }
        private bool _testPassed = true;
    
        public void Initialize()
        {
            var options = new AppiumOptions
            {
                PlatformName = "Android",
                AutomationName = "UiAutomator2",
                App = "https://testingbot.com/appium/sample.apk"
            };
            options.AddAdditionalAppiumOption("appium:deviceName", "Galaxy S24");
            options.AddAdditionalAppiumOption("appium:platformVersion", "14.0");
            options.AddAdditionalAppiumOption("tb:options", new Dictionary<string, object>
            {
                ["key"] = Environment.GetEnvironmentVariable("TB_KEY") ?? "",
                ["secret"] = Environment.GetEnvironmentVariable("TB_SECRET") ?? "",
                ["name"] = "Reqnroll Android Real Device",
                ["realDevice"] = true
            });
    
            Driver = new AndroidDriver(
                new Uri("https://hub.testingbot.com/wd/hub"),
                options,
                TimeSpan.FromSeconds(120)
            );
        }
    
        public void MarkTestFailed() => _testPassed = false;
    
        public void Dispose()
        {
            if (Driver != null)
            {
                var status = _testPassed ? "passed" : "failed";
                ((IJavaScriptExecutor)Driver).ExecuteScript($"tb:test-result={status}");
                Driver.Dispose();
            }
        }
    }

The example below runs the Reqnroll scenario on a real iPhone. Use `IOSDriver` instead of `AndroidDriver`.

    using OpenQA.Selenium;
    using OpenQA.Selenium.Appium;
    using OpenQA.Selenium.Appium.iOS;
    
    namespace AppiumReqnrollTests.Support;
    
    public class TestingBotDriver : IDisposable
    {
        public IOSDriver Driver { get; private set; }
        private bool _testPassed = true;
    
        public void Initialize()
        {
            var options = new AppiumOptions
            {
                PlatformName = "iOS",
                AutomationName = "XCUITest",
                App = "https://testingbot.com/appium/sample.ipa"
            };
            options.AddAdditionalAppiumOption("appium:deviceName", "iPhone 15");
            options.AddAdditionalAppiumOption("appium:platformVersion", "18.0");
            options.AddAdditionalAppiumOption("tb:options", new Dictionary<string, object>
            {
                ["key"] = Environment.GetEnvironmentVariable("TB_KEY") ?? "",
                ["secret"] = Environment.GetEnvironmentVariable("TB_SECRET") ?? "",
                ["name"] = "Reqnroll iOS Real Device",
                ["realDevice"] = true
            });
    
            Driver = new IOSDriver(
                new Uri("https://hub.testingbot.com/wd/hub"),
                options,
                TimeSpan.FromSeconds(300)
            );
        }
    
        public void MarkTestFailed() => _testPassed = false;
    
        public void Dispose()
        {
            if (Driver != null)
            {
                var status = _testPassed ? "passed" : "failed";
                ((IJavaScriptExecutor)Driver).ExecuteScript($"tb:test-result={status}");
                Driver.Dispose();
            }
        }
    }

## Simulator/Emulator Testing
[Android](https://testingbot.com#)[iOS](https://testingbot.com#)

Run the same Reqnroll scenario on an Android emulator. Omit `realDevice` from `tb:options` to target an emulator.

    var options = new AppiumOptions
    {
        PlatformName = "Android",
        AutomationName = "UiAutomator2",
        App = "https://testingbot.com/appium/sample.apk"
    };
    options.AddAdditionalAppiumOption("appium:deviceName", "Pixel 8");
    options.AddAdditionalAppiumOption("appium:platformVersion", "14.0");
    options.AddAdditionalAppiumOption("tb:options", new Dictionary<string, object>
    {
        ["key"] = Environment.GetEnvironmentVariable("TB_KEY") ?? "",
        ["secret"] = Environment.GetEnvironmentVariable("TB_SECRET") ?? "",
        ["name"] = "Reqnroll Android Emulator"
    });
    
    Driver = new AndroidDriver(
        new Uri("https://hub.testingbot.com/wd/hub"),
        options,
        TimeSpan.FromSeconds(240)
    );

Run the same Reqnroll scenario on an iOS simulator.

    var options = new AppiumOptions
    {
        PlatformName = "iOS",
        AutomationName = "XCUITest",
        App = "https://testingbot.com/appium/sample.zip"
    };
    options.AddAdditionalAppiumOption("appium:deviceName", "iPhone 15");
    options.AddAdditionalAppiumOption("appium:platformVersion", "17.4");
    options.AddAdditionalAppiumOption("tb:options", new Dictionary<string, object>
    {
        ["key"] = Environment.GetEnvironmentVariable("TB_KEY") ?? "",
        ["secret"] = Environment.GetEnvironmentVariable("TB_SECRET") ?? "",
        ["name"] = "Reqnroll iOS Simulator"
    });
    
    Driver = new IOSDriver(
        new Uri("https://hub.testingbot.com/wd/hub"),
        options,
        TimeSpan.FromSeconds(300)
    );

## Uploading Your App

Please see our [Upload Mobile App](https://testingbot.com/support/app-automate/help/upload) documentation to find out how to upload your app to TestingBot for testing.

## Specify Browsers & Devices

To run your existing tests on TestingBot, your tests will need to be configured to use the TestingBot remote machines. If the test was running on your local machine or network, you can simply change your existing test like this:

**Before:**

    Driver = new IOSDriver(
        new Uri("http://localhost:4723/wd/hub"),
        options
    );

**After:**

    Driver = new IOSDriver(
        new Uri("https://hub.testingbot.com/wd/hub"),
        options
    );

### Configuring Capabilities

Capabilities are key-value pairs that allow you to customize your tests on TestingBot.   
 Appium provides its [own set of capabilities](https://appium.io/docs/en/2.0/guides/caps/) which you can specify.   
 TestingBot also provides its own [Custom Capabilities](https://testingbot.com/support/app-automate/appium/options), to customize tests run on the TestingBot platform.

You can use the drop-down menus below to see how to configure your tests to run on a specific mobile device:

  ![OS selected](https://testingbot.com/assets/environments/svg/ios-383468addf160fa18d0e431f529420739d7f7d1206175f682fead627d2e99a52.svg) iOS 18.6 › iPhone 16 

Loading environments...

Please wait while we load the available browsers and platforms.

    

## Testing Internal Websites

We've built [TestingBot Tunnel](https://testingbot.com/support/tunnel), to provide you with a secure way to run tests against your staged or internal webapps.   
Please see our [TestingBot Tunnel documentation](https://testingbot.com/support/tunnel) for more information about this easy to use tunneling solution.

The example below shows how to easily run a Reqnroll test with our Tunnel:

1. [Download our tunnel](https://testingbot.com/support/tunnel) and start the tunnel:

    java -jar testingbot-tunnel.jar key secret

2. Adjust your test: instead of pointing to `'hub.testingbot.com/wd/hub'` like the example above, change it to point to your tunnel's IP address.   
 Assuming you run the tunnel on the same machine you run your tests, change to `'localhost:4445/wd/hub'`. localhost is the machine running the tunnel, 4445 is the default port of the tunnel.

This way your test will go securely through the tunnel to TestingBot and back:

    var options = new AppiumOptions
    {
        PlatformName = "Android",
        AutomationName = "UiAutomator2",
        App = "tb://..."
    };
    options.AddAdditionalAppiumOption("appium:deviceName", "Pixel 8");
    options.AddAdditionalAppiumOption("appium:platformVersion", "14.0");
    options.AddAdditionalAppiumOption("tb:options", new Dictionary<string, object>
    {
        ["name"] = "Reqnroll Tunnel Test"
    });
    
    // Point to the local tunnel instead of hub.testingbot.com
    Driver = new AndroidDriver(
        new Uri("http://localhost:4445/wd/hub"),
        options
    );

## Run tests in Parallel

Parallel Testing means running the same test, or multiple tests, simultaneously. This greatly reduces your total testing time.

Reqnroll supports parallel test execution when using NUnit or xUnit as the test runner.

### NUnit Parallel Execution

Add to your `AssemblyInfo.cs` or any file:

    using NUnit.Framework;
    
    [assembly: Parallelizable(ParallelScope.Fixtures)]
    [assembly: LevelOfParallelism(5)]

Configure in `reqnroll.json`:

    {
      "$schema": "https://schemas.reqnroll.net/reqnroll-config-latest.json",
      "bindingCulture": {
        "name": "en-US"
      }
    }

### xUnit Parallel Execution

Add `xunit.runner.json`:

    {
      "parallelizeAssembly": true,
      "parallelizeTestCollections": true,
      "maxParallelThreads": 5
    }

### Queuing

Every TestingBot plan has a limit on parallel tests. If you exceed this limit, additional tests are queued (up to 6 minutes) and run as slots become available.

## Mark tests as passed/failed

Report test results to TestingBot using JavaScript execution in your `[AfterScenario]` hook:

    [Binding]
    public class TestHooks
    {
        private readonly TestingBotDriver _tbDriver;
    
        public TestHooks(TestingBotDriver tbDriver)
        {
            _tbDriver = tbDriver;
        }
    
        [AfterScenario]
        public void AfterScenario(ScenarioContext scenarioContext)
        {
            if (_tbDriver.Driver != null)
            {
                var status = scenarioContext.TestError == null ? "passed" : "failed";
                ((IJavaScriptExecutor)_tbDriver.Driver).ExecuteScript($"tb:test-result={status}");
                _tbDriver.Driver.Dispose();
            }
        }
    }

## Migrating from SpecFlow

Reqnroll is designed as a drop-in replacement for SpecFlow. Most Appium projects can migrate with minimal changes:

### 1. Update NuGet Packages

    # Remove SpecFlow packages
    dotnet remove package SpecFlow
    dotnet remove package SpecFlow.NUnit
    
    # Add Reqnroll packages
    dotnet add package Reqnroll.NUnit

### 2. Update Namespaces

Replace `TechTalk.SpecFlow` with `Reqnroll` in your code:

    // Before (SpecFlow)
    using TechTalk.SpecFlow;
    
    // After (Reqnroll)
    using Reqnroll;

### 3. Update Configuration

Rename `specflow.json` to `reqnroll.json` and update the schema reference:

    {
      "$schema": "https://schemas.reqnroll.net/reqnroll-config-latest.json"
    }

### 4. Update Visual Studio Extension

Install the "Reqnroll for Visual Studio 2022" extension and uninstall the SpecFlow extension.

For a complete migration guide, see the [official SpecFlow to Reqnroll migration guide](https://reqnroll.net/news/2024/02/from-specflow-to-reqnroll-why-and-how/).

## Preparing your App

You do not need to install any code or plugin to run a test.   
 Below are some things that are necessary to successfully run a mobile test:

**For Android:**
- Please supply the URL to your `.apk` or `.aab` file.   
**Important:** the `.apk` file needs to be a x86 build (x86 ABI) for Android emulators.

**For iOS Real Device:**
- Please supply the URL to an `.ipa` file.

**For iOS Simulator:**
- Please supply the URL to a `.zip` file that contains your `.app`
- The `.app` needs to be an iOS Simulator build:   

  - Create a Simulator build:  

    xcodebuild -sdk iphonesimulator -configuration Debug

  - Zip the `.app` bundle:  

    zip -r MyApp.zip MyApp.app

## Additional Options

Appium provides [a lot of options](https://appium.io/docs/en/2.0/guides/caps/) to configure your test.

Some important options that might help:

**For Android:**
- **appActivity** and **appPackage** : by default, Appium will try to extract the main Activity from your apk. If this fails, you can supply your own with these options.
- **chromeOptions** : additional chromedriver options you can supply.
- **otherApps** : a JSON array of other apps that need to be installed, in URL format.

**For Android & iOS:**
- **locale** : the language of the simulator/device (ex: `fr_CA`) 

This sets the locale on the entire device/simulator, except on physical iOS devices. For real iOS devices, we will pass `-AppleLocale` as argument to the app.

- **newCommandTimeout** : How long (in seconds) Appium will wait for a new command from the client before assuming the client quit and ending the session

Was this page helpful? Yes No 

## Looking for More Help?

Have questions or need more information?   
 You can reach us via the following channels:

- [Email us](https://testingbot.com/contact/new)
- [Join our Slack Channel](https://join.slack.com/t/testingb0t/shared_invite/zt-3bcw9xch-jk19~6XPs_xBrsAgAedkCw)
