There are several methods of unit testing in Android, what's the best one for testing a custom view I've written?
I'm currently testing it as part of my activity in an instrumentation test case, but I'd rather test just the view, isolated.
A simple solution for the lack of a View-focused TestCase implementation would be to create a simple Activity within your test project that includes your view. This will allow you to write tests against the view using a simple Activity. Information on Activity testing:
http://developer.android.com/reference/android/test/ActivityUnitTestCase.html
As mentioned in wikibooks:
unit testing is a method by which individual units of source code are tested to determine if they are fit for use.
So when you say you want to test your custom view, you can check various methods of your custom views like "onTouchEvent", "onDown", "onFling", "onLongPress", "onScroll", "onShowPress", "onSingleTapUp", "onDraw" and various others depending on your business logic. You can provide mock values and test it. I would suggest two methods of testing your custom view.
1) Monkey Testing
Monkey testing is random testing performed by automated testing tools.
G.D.S. Prasad on geekinterview.com
and:
A monkey test is a unit test that runs with no specific test in mind. The monkey in this case is the producer of any input. For example, a monkey test can enter random strings into text boxes to ensure handling of all possible user input or provide garbage files to check for loading routines that have blind faith in their data.
sridharrganesan on geekinterview.com
This is a black box testing technique and it can check your custom view in so many unique conditions that you will get astonished :) .
2) Unit Testing
2a) Use Robotium Unit Testing Framwork
Go to Robotium.org or http://code.google.com/p/robotium/ and download the example test project. Robotium is a really easy to use framework that makes testing of android applications easy and fast. I created it to make testing of advanced android applications possible with minimum effort. Its used in conjunction with ActivityInstrumentationTestCase2.
2b) Use Android Testing Framework
Here are the links to the reference:
http://developer.android.com/reference/android/test/ActivityInstrumentationTestCase2.html
and
http://developer.android.com/reference/android/test/ActivityUnitTestCase.html
For starters:
http://developer.android.com/guide/topics/testing/testing_android.html
According to one user : Aside from easily testing non platform
dependent logic I haven't found a
clever way to run tests, so far (at
least for me) any actual platform
logic testing is cumbersome. It's
almost non trivial anyway because I've
found differences in implementation
between the emulator and my actual
device and I hate to run a unit test
implementation on my device just to
remove the application afterwards.
My strategy has been: Try to be
concise and make the logic well
thought out and then test
implementation piece by piece (less
then desirable).
Also Stephen Ng provides good aproach for real Unit Test for Android projects solution: https://sites.google.com/site/androiddevtesting/
One user has made a screencast.
Here's a ScreenCast I made on how I got Unit Tests to work. Simple Unit
Tests and more complex unit tests that
depend on having a reference to
Context or Activity objects.
http://www.gubatron.com/blog/2010/05/02/how-to-do-unit-testing-on-android-with-eclipse/
Hope it helps you testing your custom view in all possible conditions :)
Comment (futlib) All your suggestions seem to involve testing the ACTIVITY, while I really want to test just the VIEW. I might want to use this view in other activities, so it doesn't make much sense for me to test it with a specific one. – futlib
Answer: To implement a custom view,
you will usually begin by providing
overrides for some of the standard
methods that the framework calls on
all views. For example "onDraw",
"onKeyDown(int, KeyEvent)",
"onKeyUp(int, KeyEvent)",
"onTrackballEvent(MotionEvent)" etc of
your custom view. So when you want to
do unit testing for your custom you'll
have to test these methods, and
provide mock values to it so that you
can test your custom view on all
possible cases. Testing these methods
doesn't mean that you are testing your
ACTIVITY, but it means testing your
custom view (methods/functions) which
is within an activity. Also you'll
have to put your custom view in an
Activity eventually for your target
users to experience it. Once
thoroughly tested , your custom view
can be placed in many projects and
many activities.
Here's a different suggestion which works fine in many cases: Assuming you are referencing your custom view from within a layout file, you can use an AndroidTestCase, inflate the view, and then perform tests against it in isolation. Here's some example code:
my_custom_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<de.mypackage.MyCustomView ...
MyCustomView.java:
public class MyCustomView extends LinearLayout {
public MyCustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setTitle(CharSequence title) {
((TextView) findViewById(R.id.mylayout_title_textView)).setText(title);
}
...
MyCustomViewTest.java:
public class MyCustomViewTest extends AndroidTestCase {
private MyCustomView customView;
#SuppressLint("InflateParams")
#Override
protected void setUp() throws Exception {
super.setUp();
customView = (MyCustomView) LayoutInflater.from(getContext())
.inflate(R.layout.my_custom_layout, null);
}
public void testSetTitle_SomeValue_TextViewHasValue() {
customView.setTitle("Some value");
TextView titleTextView = (TextView) valueSelection.findViewById(R.id.mylayout_title_textView);
assertEquals("Some value", titleTextView.getText().toString());
}
...
I struggled a lot to set up screenshot tests for my custom view.
Here is how I managed to do that and everything I learned in the process.
It may not be the most convenient method, but I put it here anyway.
And of course, screenshot testing is now a little bit easier in Jetpack Compose.
⚠ Caution #1
You can use JUnit 4 if you want. I'm using JUnit 5. Because JUnit 5 is built on Java 8 from the ground up, its instrumentation tests will only run on devices running Android 8.0 (API 26) or newer. Older phones/emulators will skip the execution of these tests completely, marking them as ignored.
If you want to run JUnit 5 tests on Android, refer to this answer for how to set it up.
⚠ Caution #2
The screenshot tests may not work on other devices even if they have the same screen DPI (they may not work at all on devices with different screen DPIs). For example, even when I use the same device in my local machine and on GitHub Actions to run the tests, they do not produce the same result (GitHub Actions assertions fail). So, I had to disable them on GitHub Actions.
If you want to disable the screenshot tests on GitHub Actions (or other CI), see this answer.
⚠ Caution #3
If you have resources in instrumented tests (in androidTest source set) and you want to reference their id, you should use them like this (note the package name followed by .test):
com.example.test.R.id.an_id
For example, if your package name is my.package.name then to access the layout file in src/androidTest/res/layout/my_layout.xml in your tests, you use my.package.name.test.R.layout.my_layout.
⚠ Caution #4
Since we are saving our test screenshots on the external storage of the device/emulator, we need to make sure that we have both WRITE_EXTERNAL_STORAGE permission added in the manifest and adb install options -g and -r configured in the build script. When running on Marshmallow+, we also need to have those permissions granted before running a test. -g is for granting permissions when installing the app (works on Marshmallow+ only) while -r is to allow reinstalling of the app.
These correspond to adb shell pm install options.
Just be aware that this does not work with Android Studio yet.
So, create an AndroidManifest.xml file in src/androidTest/ directory and add the following to it:
<manifest package="my.package.name">
<!-- For saving screenshots in tests -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage"
tools:remove="android:maxSdkVersion"/>
<application android:requestLegacyExternalStorage="true">
<activity android:name=".MyActivityThatContainsTheView"/>
</application>
</manifest>
and add the adb install options in your library Gradle build file:
android {
// Note that adbOptions block is deprecated in Android Gradle Plugin 7.0.0;
// replace adbOptions block with installation block
adbOptions {
installOptions("-g", "-r")
}
}
⚠ Caution #5
I save the reference screenshot (the one I want to compare with the current screenshot) in src/androidTest/assets directory. So, specify that directory as an assets entry in the library build file:
android {
sourceSets {
// This is Kotlin DSL; see https://stackoverflow.com/a/59920318 for groovy DSL
get("debug").assets.srcDirs("src/androidTest/assets")
}
⚠ Caution #6
To pass instrumentation arguments when running the tests (like shouldSave in my code), do this:
For a Gradle task:
Running the task from command line: pass your arguments after the task name
./gradlew myTask -Pandroid.testInstrumentationRunnerArguments.shouldSave=true
Running the task with Studio: pass your arguments in run config Arguments: field
-Pandroid.testInstrumentationRunnerArguments.shouldSave=true
For an Android Studio Android Instrumented Tests run configuration:
Select Edit Configurations... from run configuration popup, then select your run configuration, click ... in front of Instrumentation arguments: field and then add a name-value entry like Name shouldSave Value true.
See this article and this post.
⚠ Caution #7
The first time you want to run the screenshot tests and also whenever you update your custom view that might change its visuals, you should run the tests passing true for shouldSave argument so the new screenshots are saved in the device (see comments above save method in code below for the location of the images) and then manually copy the new screenshots to your src/androidTest/assets/ directory so they will be the new reference ones.
⚠ Caution #8
Make sure to use -ktx versions of androidx libraries (like AndroidX Core library) for Kotlin.
The -ktx variants contain useful Kotlin extension functions. Example:
implementation("androidx.core:core-ktx:1.6.0")
⚠ Caution #9
Make sure the device screen is on and unlocked for the activity to go to resumed state.
The code
This is my test activity in src/androidTest/java/com/example/ directory that exposes the view that I want to take its screenshot as a property:
class MyActivityThatContainsTheView : AppCompatActivity() {
lateinit var myView: MyView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(my.package.name.test.R.layout.my_layout_that_contains_the_view)
myView = findViewById(my.package.name.test.R.id.my_view_id_in_the_layout_file)
}
}
And finally, this is my tests and how I save, load, and compare the screenshots:
#DisabledIfBuildConfigValue(named = "CI", matches = "true")
class ScreenshotTestView {
#JvmField
#RegisterExtension
val scenarioExtension = ActivityScenarioExtension.launch<MyActivityThatContainsTheView>()
lateinit var scenario: ActivityScenario<MyActivityThatContainsTheView>
// See ⚠ Caution #6 above in the post
val shouldSave = InstrumentationRegistry.getArguments().getString("shouldSave", "false").toBoolean()
val shouldAssert = InstrumentationRegistry.getArguments().getString("shouldAssert", "true").toBoolean()
#BeforeEach fun setUp() {
scenario = scenarioExtension.scenario
scenario.moveToState(Lifecycle.State.RESUMED)
}
#Test fun test1() {
val screenshotName = "screenshot-1"
scenario.onActivity { activity ->
val view = activity.myView
view.drawToBitmap()
.saveIfNeeded(shouldSave, screenshotName)
.assertIfNeeded(shouldAssert, screenshotName)
}
}
fun Bitmap.saveIfNeeded(shouldSave: Boolean, name: String): Bitmap {
if (shouldSave) save(name)
return this
}
fun Bitmap.assertIfNeeded(shouldCompare: Boolean, screenshotName: String) {
if (shouldCompare) assert(screenshotName)
}
/**
* The screenshots are saved in /Android/data/my.package.name.test/files/Pictures
* on the external storage of the device.
*/
private fun Bitmap.save(name: String) {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val path = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val file = File(path, "$name.png")
file.outputStream().use { stream ->
compress(Bitmap.CompressFormat.PNG, 100, stream)
}
}
private fun Bitmap.assert(screenshotName: String) {
val reference = loadReferenceScreenshot(screenshotName)
// I'm using AssertJ library; you can simply use assertTrue(this.sameAs(reference))
assertThat(this.sameAs(reference))
.withFailMessage { "Screenshots are not the same: $screenshotName.png" }
.isTrue()
}
private fun loadReferenceScreenshot(name: String): Bitmap {
val context = InstrumentationRegistry.getInstrumentation().context
val assets = context.resources.assets
val reference = assets.open("$name.png").use { stream ->
BitmapFactory.decodeStream(stream)
}
return reference
}
}
Related
I'm just trying to get started with Android development and Kotlin using Jetpack Compose. Note that, I'm a Kotlin novice, so I'm trying to learn along the way. I come from JavaScript/TypeScript background, so I'm trying to learn by thinking in JavaScript terms and implement in Kotlin terms by searching online.
I'm trying to list all installed applications on the device. While the app was working as expected up till now, I needed to add a feature to sort the installed app names. I referred to: https://www.bezkoder.com/kotlin-sort-list-objects/#Create_Class_for_handling_sorting. As soon as I added a custom class to sort the List<ApplicationInfo>, my app stopped building.
I have included my repo here: https://github.com/Hrishikesh-K/TryKotlin
If I comment these lines and this line as well, the app builds fine. With the current setup, I get an error:
Functions which invoke #Composable functions must be marked with the #Composable annotation
which points to line 21, character 18, which is the start of the word compare.
I don't understand why Compose would care about a custom class, it's not a Composable function after all. What am I missing?
In the compare method you are using LocalContext.current
override fun compare(o1 : ApplicationInfo, o2 : ApplicationInfo): Int {
return o1.loadLabel(LocalContext.current.packageManager).toString().compareTo(o2.loadLabel(LocalContext.current.packageManager).toString())
}
You can't use a #Composable functions if the method is not marked with the #Composable.
Use something different like:
data class CompareApplicationNames(val context: Context) : Comparator<ApplicationInfo> {
override fun compare(o1 : ApplicationInfo, o2 : ApplicationInfo): Int {
return o1.loadLabel(context.packageManager).toString().compareTo(o2.loadLabel(context.packageManager).toString())
}
}
Then just use:
Log.d("sorted:", listOfApplications.sortedWith(CompareApplicationNames(LocalContext.current)).toString())
I have a few "connected" tests that are only relevant to run on a specific device model or on a specific brand and should be skipped on other brands/models.
I may be missing something, but this kind of filtering seems not possible out-of-the-box with AndroidJUnitRunner (by using annotation and/or passing appropriate arguments to it).
So, I was thinking to extend the AndroidX test framework to support this kind of filtering. In the end, I would like to be able to filter test with something like this
#TargetDeviceFilter(brand="SAMSUNG",model="XCover3")
#Test
public void myTestToRunOnSamsungXCover3DeviceOnly(){
...
}
My question: is there any way to accomplish this kind of filtering without extending AndroidX test framework? And if writing my own AndroidJUnitRunner and/or my own annotations is required, how should I start ?
I found a few interesting base classes that I may need to extend like :
androidx.test.internal.runner.TestRequestBuilder
androidx.test.internal.runner.TestRequestBuilder.DeviceBuild
but as those classes are in a "internal" package: attempting to extend them is probably not a good idea?
Any advice on how to deal with that problem is welcome.
I think, you may use org.junit.Assume.
Create a helper class DeviceHelper to detect mobile device informations for convenience.
Your test logic will be executed only if the assumption is correct.
#Test
public void myTestToRunOnSamsungXCover3DeviceOnly() {
// adapt this part to your business need
org.junit.Assume.assumeTrue(
DeviceHelper.isBrand("SAMSUNG") &&
DeviceHelper.isModel("XCover3")
);
// i.e. you can filter whatever you want test's according to device sdk_int
assumeTrue(SomeHelper.getDeviceSdk() >= 21);
// your test code
}
I'm trying to learn the Arrow library and improve my functional programming by transitioning some of my Android Kotlin code from more imperative style to functional style. I've been doing a type of MVI programming in the application to make testing simpler.
"Traditional" Method
ViewModel
My view model has a LiveData of the view's state plus a public method to pass user interactions from the view to the viewmodel so the view model can update state in whatever way is appropriate.
class MyViewModel: ViewModel() {
val state = MutableLiveData(MyViewState()) // MyViewState is a data class with relevant data
fun instruct(intent: MyIntent) { // MyIntent is a sealed class of data classes representing user interactions
return when(intent) {
is FirstIntent -> return viewModelScope.launch(Dispatchers.IO) {
val result = myRoomRepository.suspendFunctionManipulatingDatabase(intent.myVal)
updateStateWithResult(result)
}.run { Unit }
is SecondIntent -> return updateStateWithResult(intent.myVal)
}
}
}
Activity
The Activity subscribes to the LiveData and, on changes to state, it runs a render function using the state. The activity also passes user interactions to the view model as intents (not to be confused with Android's Intent class).
class MyActivity: AppCompatActivity() {
private val viewModel = MyViewModel()
override fun onCreateView() {
viewModel.state.observe(this, Observer { render(it) })
myWidget.onClickObserver = {
viewModel.instruct(someIntent)
}
}
private fun render(state: MyViewState) { /* update view with state */ }
}
Arrow.IO Functional Programming
I'm having trouble finding examples that aren't way over my head using Arrow's IO monad to make impure functions with side effects obvious and unit-testable.
View Model
So far I have turned my view model into:
class MyViewModel: ViewModel() {
// ...
fun instruct(intent: MyIntent): IO<Unit> {
return when(intent) {
is FirstIntent -> IO.fx {
val (result) = effect { myRoomRepository.suspendFunctionManipulatingDatabase(intent.myVal) }
updateStateWithResult(result)
}
is SecondIntent -> IO { updateStateWithResult(intent.myVal) }
}
}
}
I do not know how I am supposed to make this IO stuff run in Dispatcher.IO like I've been doing with viewModelScope.launch. I can't find an example for how to do this with Arrow. The ones that make API calls all seem to be something other than Android apps, so there is no guidance about Android UI vs IO threads.
View model unit test
Now, because one benefit I'm seeing to this is that when I write my view model's unit tests, I can have a test. If I mock the repository in order to check whether suspendFunctionManipulatingDatabase is called with the expected parameter.
#Test
fun myTest() {
val result: IO<Unit> = viewModel.instruct(someIntent)
result.unsafeRunSync()
// verify suspendFunctionManipulatingDatabase argument was as expected
}
Activity
I do not know how to incorporate the above into my Activity.
class MyActivity: AppCompatActivity() {
private val viewModel = MyViewModel()
override fun onCreateView() {
viewModel.state.observe(this, Observer { render(it) })
myWidget.onClickObserver = {
viewModel.instruct(someIntent).unsafeRunSync() // Is this how I should do it?
}
}
// ...
}
My understanding is anything in an IO block does not run right away (i.e., it's lazy). You have to call attempt() or unsafeRunSync() to get the contents to be evaluated.
Calling viewModel.instruct from Activity means I need to create some scope and invoke in Dispatchers.IO right? Is this Bad(TM)? I was able to confine coroutines completely to the view model using the "traditional" method.
Where do I incorporate Dispatchers.IO to replicate what I did with viewModelScope.launch(Dispatchers.IO)?
Is this the way you're supposed to structure a unit test when using Arrow's IO?
That's a really good post to read indeed. I'd also recommend digging into this sample app I wrote that is using ArrowFx also.
https://github.com/JorgeCastilloPrz/ArrowAndroidSamples
Note how we build the complete program using fx and returning Kind at all levels in our architecture. That makes the code polymorphic to the type F, so you can run it using different runtime data types for F at will, depending on the environment. In this case we end up running it using IO at the edges. That's the activity in this case, but could also be the application class or a fragment. Think about this as what'd be the entry points to your apps. If we were talking about jvm programs the equivalent would be main(). This is just an example of how to write polymorphic programs, but you could use IO.fx instead and return IO everywhere, if you want to stay simpler.
Note how we use continueOn() in the data source inside the fx block to leave and come back to the main thread. Coroutine context changes are explicit in ArrowFx, so the computation jumps to the passed thread right after the continueOn until you deliberately switch again to a different one. That intentionally makes thread changes explicit.
You could inject those dispatchers to use different ones in tests. Hopefully I can provide examples of this soon in the repo, but you can probably imagine how this would look.
For the syntax on how to write tests note that your program will return Kind (if you go polymorphic) or IO, so you would unsafeRunSync it from tests (vs unsafeRunAsync or unsafeRunAsyncCancellable in production code since Android needs it to be asynchronous). That is because we want our test to be synchronous and also blocking (for the latter we need to inject the proper dispatchers).
Current caveats: The solution proposed in the repo still doesn't care of cancellation, lifecycle or surviving config changes. That's something I'd like to address soon. Using ViewModels with a hybrid style might have a chance. This is Android so I'd not fear hybrid styles if that brings better productivity. Another alternative I've got in mind would maybe be something a bit more functional. ViewModels end up retaining themselves using the retain config state existing APIs under the hood by using the ViewModelStore. That ultimately sounds like a simple cache that is definitely a side effect and could be implemented wrapped into IO. I want to give a thought to this.
I would definitely also recommend reading the complete ArrowFx docs for better understanding: https://arrow-kt.io/docs/fx/ I think it would be helpful.
For more thoughts on approaches using Functional Programming and Arrow to Android you can take a look to my blog https://jorgecastillo.dev/ my plan is to write deep content around this starting 2020, since there's a lot of people interested.
In the other hand, you can find me or any other Arrow team maintainers in the Kotlinlang JetBrains Slack, where we could have more detailed conversations or try to resolve any doubts you can have https://kotlinlang.slack.com/
As a final clarification: Functional Programming is just a paradigm that resolves generic concerns like asynchrony, threading, concurrency, dependency injection, error handling, etc. Those problems can be found on any program, regardless of the platform. Even within an Android app. That is why FP is an option as valid for mobile as any other one, but we are still into explorations to provide the best APIs to fulfill the usual Android needs in a more ergonomic way. We are in the process of exploration in this sense, and 2020 is going to be a very promising year.
Hopefully this helped! Your thoughts seem to be well aligned with how things should work in this approach overall.
Hello to all Android Developers, I need to clarify a doubt in relation to the management of dynamic resources in Android applications.
I need my application to use the translations returned by my backend depending on the language configured on the phone.
I wanted to implement it in an elegant way working on a custom LayoutInflater that applies a ViewTransformer depending on the type of graphic component.
Each ViewTransformer will only collect the identifier (for example #id/landing_welcome_text) and make the next call:
val value = attrs.getAttributeValue(index)
if (value != null && value.startsWith("#")) {
val text = view.context.resources.getString(attrs.getAttributeResourceValue(index, 0))
setTextForView(view, text)
}
A ContextWrapper has been implemented that returns my custom LayoutInflater and a Resource implementation
override fun getSystemService(name: String): Any {
return if (Context.LAYOUT_INFLATER_SERVICE == name)
CustomLayoutInflater(
LayoutInflater.from(baseContext),
this,
viewTransformerManager
)
else
super.getSystemService(name)
}
override fun getResources(): Resources = customResources
The problem is that overwriting the behavior of the Resources class is considered a deprecated strategy.
As the documentation says:
This constructor is deprecated. Resources should not be constructed by
apps. See Context.createConfigurationContext(Configuration).
class CustomResourcesWrapper constructor(
res: Resources,
private val languageStringRepo: ILanguageStringRepo
): Resources(res.assets, res.displayMetrics, res.configuration) {
#Throws(Resources.NotFoundException::class)
override fun getString(id: Int): String {
val value = getStringFromRepository(id)
return value ?: super.getString(id)
}
}
Does anyone know how I can get the same functionality without overwriting the Resources class?
Thank you very much for your help :)
I was looking into the same thing some time ago, in the end our team decided to go with Lokalise SDK.
From what I found out, overriding resources is the only way to do it. And even then it still doesn't cover all the cases, like mentioned in Lokalise documentation:
Some views are not supported when inflating from XML (Action bar, Menu
items, Android preferences, may be others), but you can still get the
latest translations via getString(), getText(), getQuantityString()
and other system methods, and set the content of these views
programmatically.
I saw a similar implementation in this library https://github.com/hamidness/restring although it wasn't nearly as complete as Lokalise. You can see how Lokalise is implemented if you include their library and switch to Project view in Android Studio, expand External Libraries and find com.lokalise.android, then you can see the decompiled .class files:
As for the constructor being deprecated - they deprecated it for the purpose of recreating the Resources when you need them for a different Configuration. But Context.createConfigurationContext doesn't let you override the source of the strings provided by resources, so I don't see any alternative.
I want to combine both Robolectric and Cucumber (JVM).
Currently I have two classes ActivityStepdefs where two step definitions for activity management are defined.
My second class is RoActivity Where for example an activity is created from it's class name, and where Robolectric will be used.
When I run RoActivityTest using RobolectricTestRunner the test in this class passes, but when I run RunCukesTest (class for running features as junit test) the code from RoActivity is not running as part of Robolectric, i.e. RunCukesTest search for features on my project and match it with a method inside ActivityStepdefs and finally this class will call a method from RoActivity
Is possible to run test with both junit both* runners?
I'm not sure but perhaps it's possible to do something like powermock, using junit rules.
In that case for which one should I have to define the rule?
*Cucumber and Robolectric
My small 5 cents.
Cucumber is mostly used for acceptance tests (correct me if you use it for unit testing) and Robolectric is mostly used for unit testing.
As for me, it is overkill to write cucumber during TDD. And Robolectric is still not android and I would run acceptance tests on real device or at least emulator.
I'am facing the same problem, after some google work, I got a solution:
#RunWith(ParameterizedRobolectricTestRunner::class)
#CucumberOptions( features = ["src/test/features/test.feature","src/test/features/others.feature"], plugin = ["pretty"])
class RunFeatures(val index: Int, val name:String) {
companion object {
#Parameters(name = "{1}")
#JvmStatic
fun features(): Collection<Array<Any>> {
val runner = Cucumber(RunFeatures::class.java)
Cucumber()
val children = runner.children
return children.mapIndexed{index, feature ->
arrayOf(index,feature.name)
}
}
}
#Test
fun runTest() {
val core = JUnitCore()
val feature = Cucumber(RunFeatures::class.java).children[index]!!
core.addListener(object: RunListener() {
override fun testFailure(failure: Failure?) {
super.testFailure(failure)
fail("$name failed:\n"+failure?.exception)
}
})
val runner = Request.runner(feature)
core.run(runner)
}
}
but seems not an pretty solution for me, can somebody help me out these problem:
must explicitly list all feature file path. but cannot use pattern such as *.feature
when failed cannot know which step failed.
parameter can only pass primitive type data,
I've get into cucumber source , but seems CucumberOptions inline Cucumber , I cannot pass it programmatically but can only use annotation .