Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# .NET build output
bin/
obj/
*.user

# Reqnroll / test artifacts
TestResults/
*.feature.cs

# BrowserStack SDK logs
log/
local.log
*.log

# IDE
.vs/
.vscode/
.idea/

# Credentials (never commit)
.bstack.env
76 changes: 74 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,74 @@
# reqnroll-nunit-appium-app-browserstack
We require the following new public repositories under the browserstack GitHub organization to host customer-facing sample projects for the BrowserStack SDK.
# Reqnroll (NUnit) + Appium with BrowserStack App Automate

Run native Android app tests on real devices on [BrowserStack App Automate](https://app-automate.browserstack.com/)
using [Reqnroll](https://reqnroll.net/) (BDD) on top of NUnit + the Appium .NET client, integrated through the
[BrowserStack C# SDK](https://www.nuget.org/packages/BrowserStack.TestAdapter) (`BrowserStack.TestAdapter`).

The SDK reads `browserstack.yml`, uploads/uses the app, provisions the device, and routes the Appium session
to the BrowserStack cloud — no changes to your test logic are required.

## Prerequisites

- A [BrowserStack account](https://www.browserstack.com/users/sign_up) (username + access key).
- [.NET SDK](https://dotnet.microsoft.com/download) 8.0 (net6.0+ is supported).
- The sample app is the public **WikipediaSample.apk**, pre-uploaded to BrowserStack and referenced in
`android/browserstack.yml` as a `bs://` URL. To use your own build, upload it via the
[App Automate upload API](https://www.browserstack.com/app-automate/rest-api) and replace the `app:` value.

## Setup

```bash
git clone <this-repo>
cd reqnroll-nunit-appium

# Configure credentials (either edit browserstack.yml or export env vars)
export BROWSERSTACK_USERNAME="YOUR_USERNAME"
export BROWSERSTACK_ACCESS_KEY="YOUR_ACCESS_KEY"
```

Credentials can be set in `android/browserstack.yml` (`userName` / `accessKey`) or via the
`BROWSERSTACK_USERNAME` / `BROWSERSTACK_ACCESS_KEY` environment variables (env vars take precedence).

This is an App Automate (mobile) sample, so the Android platform lives in its own self-contained directory
(`android/`) with its own `browserstack.yml`.

## Run Sample Test

The sample test drives **WikipediaSample.apk**: it taps "Search Wikipedia", types "BrowserStack", and asserts
that search results are listed.

```bash
cd android
dotnet restore
dotnet test --filter "TestCategory=sample-test"
```

To run every scenario in the project:

```bash
cd android
dotnet test
```

## Run Local Test

The local test drives **LocalSample.apk** to prove the BrowserStack Local tunnel is connected. It is tagged
`@sample-local-test` and is skipped by default in the sample run above. To run it, point `app:` in
`android/browserstack.yml` at a pre-uploaded `LocalSample.apk`, set `browserstackLocal: true`, then:

```bash
cd android
dotnet test --filter "TestCategory=sample-local-test"
```

The BrowserStack SDK starts and manages the Local tunnel automatically when `browserstackLocal: true` is set
in `browserstack.yml`.

## Notes / Dashboard

- View runs, video, device logs, and network logs on the
[App Automate dashboard](https://app-automate.browserstack.com/).
- `testObservability: true` also surfaces the build on
[Test Observability](https://observability.browserstack.com/).
- The driver is created with an empty `AppiumOptions` object; all capabilities (app, device, `bstack:options`)
are injected by the SDK from `browserstack.yml`.
5 changes: 5 additions & 0 deletions android/.config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"version": 1,
"isRoot": true,
"tools": {}
}
5 changes: 5 additions & 0 deletions android/Features/SampleLocalTest.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@sample-local-test
Feature: BrowserStack Local Testing (Local Sample App)
Scenario: Verify the BrowserStack Local tunnel from the device
Given I start the test on the Local Sample App
Then I should see the connection is up and running
6 changes: 6 additions & 0 deletions android/Features/SampleTest.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@sample-test
Feature: BrowserStack Sample (Wikipedia App)
Scenario: Search Wikipedia for BrowserStack
Given I try to search using the Wikipedia App
When I search with the keyword BrowserStack
Then the search results should be listed
40 changes: 40 additions & 0 deletions android/StepDefinitions/AppiumHooks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Threading;
using Reqnroll;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Android;

namespace ReqnrollAppiumBrowserStack
{
// Manages the Appium Android driver lifecycle for each scenario.
//
// The driver is created with an EMPTY AppiumOptions object against the
// BrowserStack hub. The BrowserStack SDK (BrowserStack.TestAdapter)
// intercepts the Appium driver construction and injects the app, device,
// and bstack:options capabilities from android/browserstack.yml — no
// capabilities are hardcoded here.
[Binding]
public class AppiumHooks
{
// Shared with the step definitions for the current scenario.
public static ThreadLocal<AndroidDriver<AndroidElement>> ThreadLocalDriver =
new ThreadLocal<AndroidDriver<AndroidElement>>();

[BeforeScenario(Order = 0)]
public static void Initialize()
{
AppiumOptions appiumOptions = new AppiumOptions();
ThreadLocalDriver.Value = new AndroidDriver<AndroidElement>(
new Uri("https://hub-cloud.browserstack.com/wd/hub/"), appiumOptions);
}

[AfterScenario]
public static void TearDown()
{
if (ThreadLocalDriver.IsValueCreated)
{
ThreadLocalDriver.Value?.Quit();
}
}
}
}
50 changes: 50 additions & 0 deletions android/StepDefinitions/SampleLocalTestSteps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Threading;
using System.Collections.ObjectModel;
using Reqnroll;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium.Android;
using OpenQA.Selenium.Support.UI;

namespace ReqnrollAppiumBrowserStack
{
[Binding]
public class SampleLocalTestSteps
{
private readonly AndroidDriver<AndroidElement> _driver;

public SampleLocalTestSteps()
{
_driver = AppiumHooks.ThreadLocalDriver.Value!;
}

[Given(@"I start the test on the Local Sample App")]
public void GivenIStartTestOnLocalApp()
{
AndroidElement testAction = (AndroidElement)new WebDriverWait(_driver, TimeSpan.FromSeconds(30))
.Until(ExpectedConditions.ElementToBeClickable(
By.Id("com.example.android.basicnetworking:id/test_action")));
testAction.Click();
}

[Then(@"I should see the connection is up and running")]
public void ThenIShouldSeeUpAndRunning()
{
Thread.Sleep(5000);
ReadOnlyCollection<AndroidElement> textViews =
_driver.FindElements(By.ClassName("android.widget.TextView"));

bool found = false;
foreach (AndroidElement element in textViews)
{
if (element.Text != null && element.Text.Contains("The active connection is"))
{
found = true;
break;
}
}
Assert.That(found, Is.True, "Expected the Local Sample App to report an active connection");
}
}
}
50 changes: 50 additions & 0 deletions android/StepDefinitions/SampleTestSteps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Threading;
using System.Collections.ObjectModel;
using Reqnroll;
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Android;
using OpenQA.Selenium.Support.UI;

namespace ReqnrollAppiumBrowserStack
{
[Binding]
public class SampleTestSteps
{
private readonly AndroidDriver<AndroidElement> _driver;

public SampleTestSteps()
{
_driver = AppiumHooks.ThreadLocalDriver.Value!;
}

[Given(@"I try to search using the Wikipedia App")]
public void GivenITrySearchWikipediaApp()
{
AndroidElement searchElement = (AndroidElement)new WebDriverWait(_driver, TimeSpan.FromSeconds(30))
.Until(ExpectedConditions.ElementToBeClickable(
MobileBy.AccessibilityId("Search Wikipedia")));
searchElement.Click();
}

[When(@"I search with the keyword BrowserStack")]
public void WhenISearchKeywordBrowserStack()
{
AndroidElement insertTextElement = (AndroidElement)new WebDriverWait(_driver, TimeSpan.FromSeconds(30))
.Until(ExpectedConditions.ElementToBeClickable(
By.Id("org.wikipedia.alpha:id/search_src_text")));
insertTextElement.SendKeys("BrowserStack");
Thread.Sleep(5000);
}

[Then(@"the search results should be listed")]
public void ThenSearchResultsShouldBeListed()
{
ReadOnlyCollection<AndroidElement> results =
_driver.FindElements(By.ClassName("android.widget.TextView"));
Assert.That(results.Count, Is.GreaterThan(0));
}
}
}
29 changes: 29 additions & 0 deletions android/android.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>ReqnrollAppiumBrowserStack</RootNamespace>
<AssemblyName>ReqnrollAppiumBrowserStack</AssemblyName>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<!-- BrowserStack SDK: instruments the test runner and routes the Appium
session to the BrowserStack cloud using android/browserstack.yml. -->
<PackageReference Include="BrowserStack.TestAdapter" Version="0.*" />
<PackageReference Include="BrowserStackLocal" Version="2.0.0" />

<!-- Test + BDD frameworks -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Reqnroll.NUnit" Version="3.2.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />

<!-- Appium client for Android automation -->
<PackageReference Include="Appium.WebDriver" Version="4.4.0" />
</ItemGroup>

</Project>
48 changes: 48 additions & 0 deletions android/browserstack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# =============================
# Set BrowserStack Credentials
# =============================
# Add your BrowserStack userName and accessKey here or set BROWSERSTACK_USERNAME and
# BROWSERSTACK_ACCESS_KEY as env variables.
userName: YOUR_USERNAME
accessKey: YOUR_ACCESS_KEY

# ======================
# BrowserStack Reporting
# ======================
projectName: BrowserStack Samples
buildName: appauto-reqnroll-nunit-appium
buildIdentifier: '#${BUILD_NUMBER}'
# `framework` lets the SDK send test context (name, status) to BrowserStack.
framework: reqnroll

# ==========================================
# Application under test
# ==========================================
# Pre-uploaded WikipediaSample.apk on BrowserStack (use this bs:// url verbatim).
app: bs://92d48b416632f2b1734259565ceab61b05ad0b24

# =======================================
# Platforms (Devices to test)
# =======================================
platforms:
- deviceName: Samsung Galaxy S22 Ultra
osVersion: "12.0"
platformName: android

# =======================
# Parallels per Platform
# =======================
parallelsPerPlatform: 1

source: reqnroll-nunit-appium-app-browserstack:sample-sdk:v1.0

# ======================
# Test Observability
# ======================
testObservability: true

# ===================
# Debugging features
# ===================
debug: true
networkLogs: true
Loading