Skip to content

Commit

Permalink
UltronTest & Soft assertions (#97)
Browse files Browse the repository at this point in the history
alex-tiurin authored Nov 30, 2024
1 parent d6f1bb8 commit 474e8ee
Showing 30 changed files with 852 additions and 104 deletions.
77 changes: 77 additions & 0 deletions composeApp/src/commonTest/kotlin/UltronTestFlowTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@

import com.atiurin.ultron.annotations.ExperimentalUltronApi
import com.atiurin.ultron.core.test.UltronTest
import com.atiurin.ultron.log.UltronLog
import kotlin.test.Test
import kotlin.test.assertTrue

class UltronTestFlowTest : UltronTest() {
companion object {
var order = 0
var beforeAllTestCounter = -1
var commonBeforeOrder = -1
var commonAfterOrder = -1
var afterOrder = -1

}

@OptIn(ExperimentalUltronApi::class)
override val beforeFirstTest = {
beforeAllTestCounter = order
UltronLog.info("Before Class")
}

override val beforeTest = {
commonBeforeOrder = order
order++
UltronLog.info("Before test common")
}
override val afterTest = {
commonAfterOrder = order
order++
assertTrue(afterOrder < commonAfterOrder, message = "CommonAfter block should run after 'after' test block")
UltronLog.info("After test common")
}

@Test
fun someTest1() = test {
var beforeOrder = -1
var goOrder = -1
order++
before {
beforeOrder = order
order++
UltronLog.info("Before TestMethod 1")
}.go {
goOrder = order
order++
UltronLog.info("Run TestMethod 1")
}.after {
afterOrder = order
order++
assertTrue(beforeAllTestCounter == 0, message = "beforeAllTests block should run before all test")
assertTrue(beforeAllTestCounter < commonBeforeOrder, message = "beforeAllTests block should run before commonBefore block")
assertTrue(commonBeforeOrder < beforeOrder, message = "beforeOrder block should run after commonBefore block")
assertTrue(beforeOrder < goOrder, message = "Before block should run before 'go'")
assertTrue(goOrder < afterOrder, message = "After block should run after 'go'")
}
}

@Test
fun someTest2() = test(suppressCommonBefore = true) {
before {
UltronLog.info("Before TestMethod 2")
}.after {
UltronLog.info("After TestMethod 2")
}.go {
assertTrue(beforeAllTestCounter == 0, message = "beforeAllTests block should run only once")
UltronLog.info("Run TestMethod 2")
}
}

@Test
fun simpleTest() = test {
assertTrue(beforeAllTestCounter == 0, message = "beforeAllTests block should run only once")
UltronLog.info("UltronTest simpleTest")
}
}
52 changes: 52 additions & 0 deletions composeApp/src/commonTest/kotlin/UltronTestFlowTest2.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import com.atiurin.ultron.annotations.ExperimentalUltronApi
import com.atiurin.ultron.core.test.UltronTest
import com.atiurin.ultron.log.UltronLog
import kotlin.test.Test
import kotlin.test.assertTrue

class UltronTestFlowTest2 : UltronTest() {
var order = 0
var beforeAllTestCounter = 0

@OptIn(ExperimentalUltronApi::class)
override val beforeFirstTest = {
beforeAllTestCounter = order
order++
UltronLog.info("Before Class")
}

@Test
fun someTest1() = test {
var beforeOrder = -1
var afterOrder = -1
var goOrder = -1
order++
before {
beforeOrder = order
order++
UltronLog.info("Before TestMethod 1")
}.go {
goOrder = order
order++
UltronLog.info("Run TestMethod 1")
}.after {
afterOrder = order
assertTrue(beforeAllTestCounter == 0, message = "beforeAllTests block should run before all test")
assertTrue(beforeOrder > beforeAllTestCounter, message = "Before block should run after 'Before All'")
assertTrue(beforeOrder < goOrder, message = "Before block should run before 'go'")
assertTrue(goOrder < afterOrder, message = "After block should run after 'go'")
}
}

@Test
fun someTest2() = test(suppressCommonBefore = true) {
before {
UltronLog.info("Before TestMethod 2")
}.after {
UltronLog.info("After TestMethod 2")
}.go {
assertTrue(beforeAllTestCounter == 0, message = "beforeAllTests block should run only once")
UltronLog.info("Run TestMethod 2")
}
}
}
2 changes: 1 addition & 1 deletion docs/docs/common/boolean.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 4
sidebar_position: 5
---

# Boolean result
2 changes: 1 addition & 1 deletion docs/docs/common/extension.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 1
sidebar_position: 3
---

# Ultron Extension
2 changes: 1 addition & 1 deletion docs/docs/common/listeners.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 3
sidebar_position: 4
---

# Listeners
2 changes: 1 addition & 1 deletion docs/docs/common/resulthandler.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 5
sidebar_position: 7
---

# Result handler
174 changes: 174 additions & 0 deletions docs/docs/common/ultrontest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
---
sidebar_position: 2
---

# UltronTest

`UltronTest` is a powerful base class provided by the Ultron framework that enables the definition of common preconditions and postconditions for tests. By extending this class, you can streamline test setup and teardown, ensuring consistent execution across your test suite.

## Features of `UltronTest`

- **Pre-Test Actions:** Define actions to be executed before each test.
- **Post-Test Actions:** Define actions to be executed after each test.
- **Lifecycle Management:** Execute code once before all tests in a class using `beforeFirstTest`.
- **Customizable Test Execution:** Suppress pre-test or post-test actions when needed.

### Example

Here is an example of using `UltronTest`:

```kotlin
class SampleUltronFlowTest : UltronTest() {

@OptIn(ExperimentalUltronApi::class)
override val beforeFirstTest = {
UltronLog.info("Before Class")
}

override val beforeTest = {
UltronLog.info("Before test common")
}

override val afterTest = {
UltronLog.info("After test common")
}

/**
* The order of method execution is as follows::
* beforeFirstTest, beforeTest, before, go, after, afterTest
*/
@Test
fun someTest1() = test {
before {
UltronLog.info("Before TestMethod 1")
}.go {
UltronLog.info("Run TestMethod 1")
}.after {
UltronLog.info("After TestMethod 1")
}
}

/**
* An order of methods execution is follow: before, go, after
* `beforeFirstTest` - Not executed, as it is only run once and was already executed before `someTest1`.
* `beforeTest` - Not executed because it was suppressed using `suppressCommonBefore`.
* `afterTest` - Not executed because it was suppressed using `suppressCommonAfter`.
*/
@Test
fun someTest2() = test(
suppressCommonBefore = true,
suppressCommonAfter = true
) {
before {
UltronLog.info("Before TestMethod 2")
}.go {
UltronLog.info("Run TestMethod 2")
}.after {
UltronLog.info("After TestMethod 2")
}
}

/**
* An order of methods execution is follow: beforeTest, test, afterTest
* `beforeFirstTest` - Not executed, since it was executed before `someTest1`
*/
@Test
fun someTest3() = test {
UltronLog.info("UltronTest simpleTest")
}
}
```

### Key Methods

- **`beforeFirstTest`**: Code executed once before all tests in a class.
- **`beforeTest`**: Code executed before each test.
- **`afterTest`**: Code executed after each test.
- **`test`**: Executes a test with options to suppress pre-test or post-test actions.

### Key Features of the `test` Method

- **Test Context Recreation:**
The `test` method automatically recreates the `UltronTestContext` for each test execution, ensuring a clean and isolated state for the test context.

- **Soft Assertion Reset:**
Any exceptions captured during `softAssertions` in the previous test are cleared at the start of each new `test` execution, maintaining a clean state.

- **Lifecycle Management:**
It invokes `beforeTest` and `afterTest` methods around your test logic unless explicitly suppressed.
---

### Purpose of `before`, `go`, and `after`
- **`before`:** Defines preconditions or setup actions that must be performed before the main test logic is executed.
These actions might include preparing data, navigating to a specific screen, or setting up the environment.
```kotlin
before {
UltronLog.info("Setting up preconditions for TestMethod 2")
}
```

- **`go`:** Encapsulates the core logic or actions of the test. This is where the actual operations being tested are performed, such as interacting with UI elements or executing specific functionality.
```kotlin
go {
UltronLog.info("Executing the main logic of TestMethod 2")
}
```

- **`after`:** Block is used for postconditions or cleanup actions that need to occur after the main test logic has executed. This might include verifying results, resetting the environment, or clearing resources.
```kotlin
after {
UltronLog.info("Cleaning up after TestMethod 2")
}
```

These methods help clearly separate test phases, making tests easier to read and maintain.

## Using `softAssertion` for Flexible Error Handling

The `softAssertion` mechanism in Ultron allows tests to catch and verify multiple exceptions during their execution without failing immediately. This feature is particularly useful for validating multiple conditions within a single test.
### Example of `softAssertion`

```kotlin
class SampleTest : UltronTest() {
@Test
fun softAssertionTest() {
softAssertion(failOnException = false) {
hasText("NotExistText").withTimeout(100).assertIsDisplayed()
hasTestTag("NotExistTestTag").withTimeout(100).assertHasClickAction()
}
verifySoftAssertions()
}
}
```

The `softAssertion` mechanism does not inherently depend on `UltronTest`.
You can use `softAssertion` independently of the `UltronTest` base class. However, in such cases, you must manually clear exceptions between tests to ensure they do not persist across test executions.
```kotlin
class SampleTest {
@Test
fun softAssertionTest() {
UltronCommonConfig.testContext.softAnalyzer.clear()
softAssertion() {
//assert smth
}
}
}
```

### Explanation

- **Fail on Exception:** By default (`failOnException = true`), `softAssertion` will throw an exception after completing all operations within its block if any failures occur.
- **Manual Verification:** If `failOnException` is set to `false`, you can explicitly verify all caught exceptions at the end of the test using `verifySoftAssertions()`.

This approach ensures granular control over how exceptions are handled and reported, making it easier to analyze and debug test failures.

---

## Benefits of `UltronTest` usage

- Simplifies test setup and teardown with consistent preconditions and postconditions.
- Enhances error handling by allowing multiple assertions within a single test.
- Improves test readability and maintainability.

By leveraging `UltronTest` and `softAssertion`, you can build robust and flexible UI tests for your applications.

10 changes: 0 additions & 10 deletions docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -7,12 +7,13 @@ import com.atiurin.sampleapp.managers.AccountManager
import com.atiurin.ultron.allure.config.UltronAllureConfig
import com.atiurin.ultron.core.compose.config.UltronComposeConfig
import com.atiurin.ultron.core.config.UltronConfig
import com.atiurin.ultron.core.test.UltronTest
import com.atiurin.ultron.testlifecycle.rulesequence.RuleSequence
import com.atiurin.ultron.testlifecycle.setupteardown.SetUpRule
import org.junit.BeforeClass
import org.junit.Rule

abstract class BaseTest {
abstract class BaseTest : UltronTest(){
val setupRule = SetUpRule("Login user rule")
.add(name = "Login valid user $CURRENT_USER") {
AccountManager(InstrumentationRegistry.getInstrumentation().targetContext).login(
Original file line number Diff line number Diff line change
@@ -23,6 +23,8 @@ import com.atiurin.sampleapp.framework.ultronext.hasProgress
import com.atiurin.sampleapp.framework.utils.AssertUtils
import com.atiurin.sampleapp.pages.ComposeElementsPage
import com.atiurin.sampleapp.tests.BaseTest
import com.atiurin.ultron.core.common.assertion.softAssertion
import com.atiurin.ultron.core.common.assertion.verifySoftAssertions
import com.atiurin.ultron.core.common.options.ClickOption
import com.atiurin.ultron.core.common.options.ContentDescriptionContainsOption
import com.atiurin.ultron.core.common.options.PerformCustomBlockOption
@@ -34,7 +36,59 @@ import com.atiurin.ultron.core.compose.nodeinteraction.click
import com.atiurin.ultron.core.compose.operation.ComposeOperationType
import com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams
import com.atiurin.ultron.core.compose.option.ComposeSwipeOption
import com.atiurin.ultron.extensions.*
import com.atiurin.ultron.core.config.UltronCommonConfig
import com.atiurin.ultron.extensions.assertContentDescriptionContains
import com.atiurin.ultron.extensions.assertContentDescriptionEquals
import com.atiurin.ultron.extensions.assertDoesNotExist
import com.atiurin.ultron.extensions.assertExists
import com.atiurin.ultron.extensions.assertHasClickAction
import com.atiurin.ultron.extensions.assertHasNoClickAction
import com.atiurin.ultron.extensions.assertHeightIsAtLeast
import com.atiurin.ultron.extensions.assertHeightIsEqualTo
import com.atiurin.ultron.extensions.assertIsDisplayed
import com.atiurin.ultron.extensions.assertIsEnabled
import com.atiurin.ultron.extensions.assertIsNotEnabled
import com.atiurin.ultron.extensions.assertIsNotFocused
import com.atiurin.ultron.extensions.assertIsNotSelected
import com.atiurin.ultron.extensions.assertIsOff
import com.atiurin.ultron.extensions.assertIsSelectable
import com.atiurin.ultron.extensions.assertIsToggleable
import com.atiurin.ultron.extensions.assertTextContains
import com.atiurin.ultron.extensions.assertTextEquals
import com.atiurin.ultron.extensions.assertValueEquals
import com.atiurin.ultron.extensions.assertWidthIsAtLeast
import com.atiurin.ultron.extensions.assertWidthIsEqualTo
import com.atiurin.ultron.extensions.captureToImage
import com.atiurin.ultron.extensions.clearText
import com.atiurin.ultron.extensions.click
import com.atiurin.ultron.extensions.clickBottomCenter
import com.atiurin.ultron.extensions.clickBottomLeft
import com.atiurin.ultron.extensions.clickBottomRight
import com.atiurin.ultron.extensions.clickCenterLeft
import com.atiurin.ultron.extensions.clickCenterRight
import com.atiurin.ultron.extensions.clickTopCenter
import com.atiurin.ultron.extensions.clickTopLeft
import com.atiurin.ultron.extensions.clickTopRight
import com.atiurin.ultron.extensions.copyText
import com.atiurin.ultron.extensions.doubleClick
import com.atiurin.ultron.extensions.execute
import com.atiurin.ultron.extensions.getNode
import com.atiurin.ultron.extensions.getNodeConfigProperty
import com.atiurin.ultron.extensions.getText
import com.atiurin.ultron.extensions.inputText
import com.atiurin.ultron.extensions.longClick
import com.atiurin.ultron.extensions.pasteText
import com.atiurin.ultron.extensions.perform
import com.atiurin.ultron.extensions.performMouseInput
import com.atiurin.ultron.extensions.replaceText
import com.atiurin.ultron.extensions.selectText
import com.atiurin.ultron.extensions.setProgress
import com.atiurin.ultron.extensions.swipeDown
import com.atiurin.ultron.extensions.swipeLeft
import com.atiurin.ultron.extensions.swipeRight
import com.atiurin.ultron.extensions.swipeUp
import com.atiurin.ultron.extensions.typeText
import com.atiurin.ultron.extensions.withTimeout
import org.junit.Assert
import org.junit.Ignore
import org.junit.Rule
@@ -76,10 +130,7 @@ class ComposeUIElementsTest : BaseTest() {

@Test
fun clickCheckBox() {
hasTestTag(simpleCheckbox)
.assertIsOff()
.click()
.assertIsOn()
hasTestTag(simpleCheckbox).assertIsOff().click().assertIsOn()
}

@Test
@@ -205,11 +256,7 @@ class ComposeUIElementsTest : BaseTest() {

@Test
fun setSelection() {
page.editableText.replaceText("qwerty")
.setSelection(0, 3, true)
.assertIsDisplayed()
.cutText()
.assertTextContains("rty")
page.editableText.replaceText("qwerty").setSelection(0, 3, true).assertIsDisplayed().cutText().assertTextContains("rty")
}

@Test
@@ -220,8 +267,7 @@ class ComposeUIElementsTest : BaseTest() {

@Test
fun setProgress() {
page.progressBar.setProgress(0.7f)
.assertIsDisplayed()
page.progressBar.setProgress(0.7f).assertIsDisplayed()
page.status.assertTextEquals("set progress 0.7")
}

@@ -251,15 +297,15 @@ class ComposeUIElementsTest : BaseTest() {

@Test
fun semanticsMatcher_performDeprecated() {
val text = page.status.perform<String>( {
val text = page.status.perform<String>({
it.fetchSemanticsNode().config[SemanticsProperties.Text].first().text
}, option = PerformCustomBlockOption(ComposeOperationType.CUSTOM, ""))
Assert.assertTrue(text.isNotBlank())
}

@Test
fun ultronComposeSemanticsNodeInteraction_performDeprecated() {
val text = page.status.assertExists().perform( option = PerformCustomBlockOption(ComposeOperationType.CUSTOM, "")) {
val text = page.status.assertExists().perform(option = PerformCustomBlockOption(ComposeOperationType.CUSTOM, "")) {
it.fetchSemanticsNode().config[SemanticsProperties.Text].first().text
}
Assert.assertTrue(text.isNotBlank())
@@ -288,13 +334,13 @@ class ComposeUIElementsTest : BaseTest() {
}

@Test
fun getNode_exits(){
fun getNode_exits() {
val node = page.status.getNode()
Assert.assertEquals(ComposeElementsActivity.Constants.statusText, node.config[SemanticsProperties.TestTag])
}

@Test
fun getNodeConfigProperty_exist(){
fun getNodeConfigProperty_exist() {
val testTag = page.status.getNodeConfigProperty(SemanticsProperties.TestTag)
Assert.assertEquals(ComposeElementsActivity.Constants.statusText, testTag)
}
@@ -487,9 +533,7 @@ class ComposeUIElementsTest : BaseTest() {
fun assertTextEquals_editableNotEmpty_includeEditableFalse() {
val text = "editable text"
AssertUtils.assertException {
page.editableText.withTimeout(100)
.replaceText(text)
.assertTextEquals("Label", text, option = TextEqualsOption(false))
page.editableText.withTimeout(100).replaceText(text).assertTextEquals("Label", text, option = TextEqualsOption(false))
}
}

@@ -518,64 +562,47 @@ class ComposeUIElementsTest : BaseTest() {
@Test
fun assertTextContains_substringTrue_validSubstringProvided() {
val text = "some text"
page.editableText
.replaceText(text)
.assertTextContains(text.substring(0, 4), TextContainsOption(substring = true))
page.editableText.replaceText(text).assertTextContains(text.substring(0, 4), TextContainsOption(substring = true))
}

@Test
fun assertTextContains_substringTrue_wrongSubstringProvided() {
AssertUtils.assertException {
page.editableText
.replaceText("valid text")
.withTimeout(100)
.assertTextContains("wrong text", TextContainsOption(substring = true))
page.editableText.replaceText("valid text").withTimeout(100).assertTextContains("wrong text", TextContainsOption(substring = true))
}
}

@Test
fun assertTextContains_substringFalse_validSubstringProvided() {
val text = "some text"
AssertUtils.assertException {
page.editableText
.replaceText(text)
.withTimeout(100)
.assertTextContains(text.substring(0, 4), TextContainsOption(substring = false))
page.editableText.replaceText(text).withTimeout(100).assertTextContains(text.substring(0, 4), TextContainsOption(substring = false))
}
}

@Test
fun assertTextContains_ignoreCase_lowercase() {
val text = "SoMe TexT"
page.editableText
.replaceText(text)
.assertTextContains(text.lowercase(), TextContainsOption(ignoreCase = true))
page.editableText.replaceText(text).assertTextContains(text.lowercase(), TextContainsOption(ignoreCase = true))
}

@Test
fun assertTextContains_ignoreCase_uppercase() {
val text = "SoMe TexT"
page.editableText
.replaceText(text)
.assertTextContains(text.uppercase(), TextContainsOption(ignoreCase = true))
page.editableText.replaceText(text).assertTextContains(text.uppercase(), TextContainsOption(ignoreCase = true))
}

@Test
fun assertTextContains_ignoreCase_and_substring() {
val text = "SoMe TexT"
page.editableText
.replaceText(text)
.assertTextContains(text.substring(0, 4).lowercase(), TextContainsOption(substring = true, ignoreCase = true))
page.editableText.replaceText(text).assertTextContains(text.substring(0, 4).lowercase(), TextContainsOption(substring = true, ignoreCase = true))
}

@Test
fun assertTextContains_ignoreCaseFalse() {
val text = "SoMe TexT"
AssertUtils.assertException {
page.editableText
.replaceText(text)
.withTimeout(100)
.assertTextContains(text.lowercase(), TextContainsOption(ignoreCase = false))
page.editableText.replaceText(text).withTimeout(100).assertTextContains(text.lowercase(), TextContainsOption(ignoreCase = false))
}
}

@@ -621,9 +648,7 @@ class ComposeUIElementsTest : BaseTest() {
@Test
fun assertContentDescriptionContains_ignoreCaseFalse() {
AssertUtils.assertException {
page.likesCounter
.withTimeout(100)
.assertContentDescriptionContains(likesCounterContentDesc.lowercase(), ContentDescriptionContainsOption(ignoreCase = false))
page.likesCounter.withTimeout(100).assertContentDescriptionContains(likesCounterContentDesc.lowercase(), ContentDescriptionContainsOption(ignoreCase = false))
}
}

@@ -640,16 +665,13 @@ class ComposeUIElementsTest : BaseTest() {

@Test
fun assertRangeInfoEquals() {
page.progressBar.setProgress(0.7f)
.assertRangeInfoEquals(ProgressBarRangeInfo(0.7f, range = 0f..0.7f, 100))
page.progressBar.setProgress(0.7f).assertRangeInfoEquals(ProgressBarRangeInfo(0.7f, range = 0f..0.7f, 100))
}

@Test
fun assertRangeInfoEquals_invalidInfo() {
AssertUtils.assertException {
page.progressBar
.setProgress(0.7f).withTimeout(100)
.assertRangeInfoEquals(ProgressBarRangeInfo(0.0f, range = 0f..0.0f, 100))
page.progressBar.setProgress(0.7f).withTimeout(100).assertRangeInfoEquals(ProgressBarRangeInfo(0.0f, range = 0f..0.0f, 100))
}
}

@@ -701,11 +723,14 @@ class ComposeUIElementsTest : BaseTest() {

@Test
fun assertMatches_invalid() {
AssertUtils.assertException { page.editableText.replaceText("some text").withTimeout(100).assertMatches(hasText("invalid text")) }
AssertUtils.assertException {
page.editableText.replaceText("some text").withTimeout(100)
.assertMatches(hasText("invalid text"))
}
}

@Test
fun customPerformParamsMapping(){
fun customPerformParamsMapping() {
val params = UltronComposeOperationParams(
operationName = "operationName",
operationDescription = "operationDescription",
@@ -716,8 +741,25 @@ class ComposeUIElementsTest : BaseTest() {
Assert.assertEquals(params.operationName, op.name)
Assert.assertEquals(params.operationDescription, op.description)
Assert.assertEquals(params.operationType, op.type)
}.perform(params){
}.perform(params) {
it.assertTextContains("Some invalid text")
}
}

@Test
fun softAssertionTest() {
UltronCommonConfig.testContext.softAnalyzer.clear()
softAssertion(false) {
hasText("NotExistText").withTimeout(100).assertIsDisplayed()
hasTestTag("NotExistTestTag").withTimeout(100).assertHasClickAction()
}
runCatching {
verifySoftAssertions()
}.onFailure { exception ->
val message = exception.message ?: throw RuntimeException("Empty exception message: $exception")
Assert.assertTrue(message.contains("NotExistText"))
Assert.assertTrue(message.contains("NotExistTestTag"))
}

}
}
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import com.atiurin.sampleapp.framework.Log
import com.atiurin.sampleapp.framework.utils.AssertUtils
import com.atiurin.sampleapp.pages.UiElementsPage
import com.atiurin.sampleapp.tests.UiElementsTest
import com.atiurin.ultron.core.common.UltronDefaultOperationResultAnalyzer
import com.atiurin.ultron.core.common.resultanalyzer.UltronDefaultOperationResultAnalyzer
import com.atiurin.ultron.core.config.UltronCommonConfig
import com.atiurin.ultron.core.config.UltronConfig
import com.atiurin.ultron.core.espresso.EspressoOperationResult
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.atiurin.sampleapp.tests.testlifecycle

import com.atiurin.sampleapp.tests.BaseTest
import com.atiurin.ultron.annotations.ExperimentalUltronApi
import com.atiurin.ultron.log.UltronLog
import org.junit.Test

class UltronTestFlowTest : BaseTest() {
companion object {
var order = 0
var beforeAllTestCounter = -1
var commonBeforeOrder = -1
var commonAfterOrder = -1
var afterOrder = -1

}

@OptIn(ExperimentalUltronApi::class)
override val beforeFirstTest = {
beforeAllTestCounter = order
UltronLog.info("Before Class")
}

override val beforeTest = {
commonBeforeOrder = order
order++
UltronLog.info("Before test common")
}
override val afterTest = {
commonAfterOrder = order
order++
assert(afterOrder < commonAfterOrder, lazyMessage = { "CommonAfter block should run after 'after' test block" })
UltronLog.info("After test common")
}

@Test
fun someTest1() = test {
var beforeOrder = -1
var goOrder = -1
order++
before {
beforeOrder = order
order++
UltronLog.info("Before TestMethod 1")
}.go {
goOrder = order
order++
UltronLog.info("Run TestMethod 1")
}.after {
afterOrder = order
order++
assert(beforeAllTestCounter == 0, lazyMessage = { "beforeAllTests block should run before all test" })
assert(beforeAllTestCounter < commonBeforeOrder, lazyMessage = { "beforeAllTests block should run before commonBefore block" })
assert(commonBeforeOrder < beforeOrder, lazyMessage = { "beforeOrder block should run after commonBefore block" })
assert(beforeOrder < goOrder, lazyMessage = { "Before block should run before 'go'" })
assert(goOrder < afterOrder, lazyMessage = { "After block should run after 'go'" })
}
}

@Test
fun someTest2() = test(suppressCommonBefore = true) {
before {
UltronLog.info("Before TestMethod 2")
}.after {
UltronLog.info("After TestMethod 2")
}.go {
assert(beforeAllTestCounter == 0, lazyMessage = { "beforeAllTests block should run only once" })
UltronLog.info("Run TestMethod 2")
}
}

@Test
fun simpleTest() = test {
assert(beforeAllTestCounter == 0, lazyMessage = { "beforeAllTests block should run only once" })
UltronLog.info("UltronTest simpleTest")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.atiurin.sampleapp.tests.testlifecycle

import com.atiurin.sampleapp.tests.BaseTest
import com.atiurin.ultron.annotations.ExperimentalUltronApi
import com.atiurin.ultron.log.UltronLog
import org.junit.Test

class UltronTestFlowTest2 : BaseTest() {
var order = 0
var beforeAllTestCounter = 0
@OptIn(ExperimentalUltronApi::class)
override val beforeFirstTest = {
beforeAllTestCounter = order
order++
UltronLog.info("Before Class")
}
@Test
fun someTest1() = test {
var beforeOrder = -1
var afterOrder = -1
var goOrder = -1
order++
before {
beforeOrder = order
order++
UltronLog.info("Before TestMethod 1")
}.go {
goOrder = order
order++
UltronLog.info("Run TestMethod 1")
}.after {
afterOrder = order
assert(beforeAllTestCounter == 0, lazyMessage = { "beforeAllTests block should run before all test" })
assert(beforeOrder > beforeAllTestCounter, lazyMessage = { "Before block should run after 'Before All'" })
assert(beforeOrder < goOrder, lazyMessage = { "Before block should run before 'go'" })
assert(goOrder < afterOrder, lazyMessage = { "After block should run after 'go'" })
}
}

@Test
fun someTest2() = test(suppressCommonBefore = true) {
before {
UltronLog.info("Before TestMethod 2")
}.after {
UltronLog.info("After TestMethod 2")
}.go {
assert(beforeAllTestCounter == 0, lazyMessage = { "beforeAllTests block should run only once" })
UltronLog.info("Run TestMethod 2")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.atiurin.sampleapp.tests.testlifecycle

import org.junit.runner.RunWith
import org.junit.runners.Suite

@RunWith(Suite::class)
@Suite.SuiteClasses(
UltronTestFlowTest::class,
UltronTestFlowTest2::class,
)
class UltronTestPlan
Original file line number Diff line number Diff line change
@@ -18,8 +18,8 @@ import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObjectNotFoundException
import com.atiurin.ultron.core.common.Operation
import com.atiurin.ultron.core.common.OperationResult
import com.atiurin.ultron.core.common.OperationResultAnalyzer
import com.atiurin.ultron.core.common.UltronDefaultOperationResultAnalyzer
import com.atiurin.ultron.core.common.resultanalyzer.OperationResultAnalyzer
import com.atiurin.ultron.core.common.resultanalyzer.UltronDefaultOperationResultAnalyzer
import com.atiurin.ultron.core.espresso.EspressoOperationResult
import com.atiurin.ultron.core.espresso.UltronEspressoOperation
import com.atiurin.ultron.core.espresso.assertion.EspressoAssertionType
@@ -38,7 +38,6 @@ import com.atiurin.ultron.listeners.LogLifecycleListener
import com.atiurin.ultron.listeners.UltronLifecycleListener
import com.atiurin.ultron.log.UltronLog
import com.atiurin.ultron.log.UltronLogcatLogger
import com.atiurin.ultron.log.getFileLogger
import com.atiurin.ultron.testlifecycle.setupteardown.ConditionExecutorWrapper
import com.atiurin.ultron.testlifecycle.setupteardown.ConditionsExecutor
import junit.framework.AssertionFailedError
@@ -148,7 +147,7 @@ object UltronConfig {
var RECYCLER_VIEW_ITEM_SEARCH_LIMIT = -1
var INCLUDE_VIEW_HIERARCHY_TO_EXCEPTION = false //where it applicable

var resultAnalyzer: OperationResultAnalyzer = UltronDefaultOperationResultAnalyzer()
var resultAnalyzer: OperationResultAnalyzer = UltronCommonConfig.resultAnalyzer

inline fun setResultAnalyzer(crossinline block: (OperationResult<Operation>) -> Boolean) {
resultAnalyzer = object : OperationResultAnalyzer {
@@ -208,10 +207,9 @@ object UltronConfig {
AssertionFailedError::class.java,
RuntimeException::class.java
)
val resultHandler: (WebOperationResult<WebInteractionOperation<*>>) -> Unit =
{
resultAnalyzer.analyze(it)
}
val resultHandler: (WebOperationResult<WebInteractionOperation<*>>) -> Unit = {
resultAnalyzer.analyze(it)
}
}
}
}
@@ -221,7 +219,7 @@ object UltronConfig {
var UIAUTOMATOR_OPERATION_POLLING_TIMEOUT = UltronCommonConfig.Defaults.POLLING_TIMEOUT_MS
var OPERATION_TIMEOUT = UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS

var resultAnalyzer: OperationResultAnalyzer = UltronDefaultOperationResultAnalyzer()
var resultAnalyzer: OperationResultAnalyzer = UltronCommonConfig.resultAnalyzer

inline fun setResultAnalyzer(crossinline block: (OperationResult<Operation>) -> Boolean) {
resultAnalyzer = object : OperationResultAnalyzer {
@@ -258,10 +256,9 @@ object UltronConfig {
UiObjectNotFoundException::class.java,
NullPointerException::class.java,
)
val resultHandler: (UiAutomatorOperationResult<UiAutomatorUiSelectorOperation>) -> Unit =
{
resultAnalyzer.analyze(it)
}
val resultHandler: (UiAutomatorOperationResult<UiAutomatorUiSelectorOperation>) -> Unit = {
resultAnalyzer.analyze(it)
}
}
}

@@ -276,10 +273,9 @@ object UltronConfig {
UiObjectNotFoundException::class.java,
NullPointerException::class.java,
)
val resultHandler: (UiAutomatorOperationResult<UiAutomatorOperation>) -> Unit =
{
resultAnalyzer.analyze(it)
}
val resultHandler: (UiAutomatorOperationResult<UiAutomatorOperation>) -> Unit = {
resultAnalyzer.analyze(it)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.atiurin.ultron.annotations

import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
import kotlin.annotation.AnnotationTarget.CLASS
import kotlin.annotation.AnnotationTarget.CONSTRUCTOR
import kotlin.annotation.AnnotationTarget.FIELD
import kotlin.annotation.AnnotationTarget.FUNCTION
import kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE
import kotlin.annotation.AnnotationTarget.PROPERTY
import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
import kotlin.annotation.AnnotationTarget.TYPEALIAS
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER

@RequiresOptIn
@MustBeDocumented
@Target(
CLASS,
ANNOTATION_CLASS,
PROPERTY,
FIELD,
LOCAL_VARIABLE,
VALUE_PARAMETER,
CONSTRUCTOR,
FUNCTION,
PROPERTY_GETTER,
PROPERTY_SETTER,
TYPEALIAS
)
@Retention(AnnotationRetention.BINARY)
public annotation class ExperimentalUltronApi
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.atiurin.ultron.core.common.assertion

import com.atiurin.ultron.core.config.UltronCommonConfig
import com.atiurin.ultron.log.UltronLog

fun softAssertion(failOnException: Boolean = true, block: () -> Unit){
UltronLog.info("Start soft assertion context")
with(UltronCommonConfig.testContext){
softAssertion = true
block()
softAssertion = false
if (failOnException){
softAnalyzer.verify()
}
}
UltronLog.info("Finish soft assertion context")
}

fun verifySoftAssertions(){
UltronCommonConfig.testContext.softAnalyzer.verify()
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
package com.atiurin.ultron.core.common
package com.atiurin.ultron.core.common.resultanalyzer

import com.atiurin.ultron.core.common.Operation
import com.atiurin.ultron.core.common.OperationResult

class CheckOperationResultAnalyzer : OperationResultAnalyzer {
override fun <Op : Operation, OpRes : OperationResult<Op>> analyze(operationResult: OpRes): Boolean {
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.atiurin.ultron.core.common.resultanalyzer

import com.atiurin.ultron.core.common.Operation
import com.atiurin.ultron.core.common.OperationResult
import com.atiurin.ultron.exceptions.UltronOperationException

class DefaultSoftAssertionOperationResultAnalyzer : SoftAssertionOperationResultAnalyzer {
private val caughtExceptions = mutableListOf<Throwable>()
lateinit var originalAnalyzer: OperationResultAnalyzer

override fun <Op : Operation, OpRes : OperationResult<Op>> analyze(operationResult: OpRes): Boolean {
return kotlin.runCatching {
originalAnalyzer.analyze(operationResult)
}.onFailure { ex ->
caughtExceptions.add(ex)
}.isSuccess
}

override fun clear(){
caughtExceptions.clear()
}

override fun verify(){
val message = StringBuilder()
if (caughtExceptions.isNotEmpty()){
val delimiter = "========================================================================================"
caughtExceptions.forEach { ex ->
message.appendLine(ex.message)
message.appendLine(delimiter)
}
clear()
throw UltronOperationException("SOFT ASSERTION FAILED. Details:\n$message")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
package com.atiurin.ultron.core.common
package com.atiurin.ultron.core.common.resultanalyzer

import com.atiurin.ultron.core.common.Operation
import com.atiurin.ultron.core.common.OperationResult

interface OperationResultAnalyzer {
/**
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.atiurin.ultron.core.common.resultanalyzer

interface SoftAssertionOperationResultAnalyzer : OperationResultAnalyzer {
/**
* Clears all previously caught exceptions, effectively resetting the internal state.
* Use this method when starting a new set of assertions to ensure
* that previous exceptions do not affect the current verification process.
*/
fun clear()

/**
* Verifies whether any exceptions were caught during previous operations.
* If there were caught exceptions, this method throws a general exception summarizing them.
* Use this method at the end of your test or operation to ensure that all assertions passed.
*
* @throws Exception if one or more exceptions were previously caught.
*/
fun verify()
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.atiurin.ultron.core.common
package com.atiurin.ultron.core.common.resultanalyzer

import com.atiurin.ultron.core.common.Operation
import com.atiurin.ultron.core.common.OperationResult
import com.atiurin.ultron.core.common.assertion.isEmptyAssertion
import com.atiurin.ultron.exceptions.UltronException
import com.atiurin.ultron.exceptions.UltronOperationException

class UltronDefaultOperationResultAnalyzer : OperationResultAnalyzer {
open class UltronDefaultOperationResultAnalyzer : OperationResultAnalyzer {
override fun <Op : Operation, OpRes : OperationResult<Op>> analyze(operationResult: OpRes): Boolean {
if (!operationResult.success) {
val exceptionToThrow = operationResult.exceptions.lastOrNull()
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package com.atiurin.ultron.core.config

import com.atiurin.ultron.core.common.UltronOperationType
import com.atiurin.ultron.core.test.context.DefaultUltronTestContext
import com.atiurin.ultron.core.test.context.UltronTestContext
import com.atiurin.ultron.core.common.resultanalyzer.OperationResultAnalyzer
import com.atiurin.ultron.core.common.resultanalyzer.UltronDefaultOperationResultAnalyzer
import com.atiurin.ultron.listeners.AbstractListenersContainer
import com.atiurin.ultron.listeners.UltronLifecycleListener

object UltronCommonConfig : AbstractListenersContainer<UltronLifecycleListener>() {
val operationsExcludedFromListeners: MutableList<UltronOperationType> = mutableListOf()
var operationTimeoutMs : Long = 5_000
var operationTimeoutMs: Long = 5_000
var isListenersOn = true
var logDateFormat = "MM-dd HH:mm:ss.SSS"
var logToFile: Boolean = true
var resultAnalyzer: OperationResultAnalyzer = UltronDefaultOperationResultAnalyzer()
var testContext: UltronTestContext = DefaultUltronTestContext()

class Defaults {
companion object {
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.atiurin.ultron.core.test

import com.atiurin.ultron.core.config.UltronCommonConfig
import com.atiurin.ultron.core.test.context.UltronTestContext

class TestMethod(testContext: UltronTestContext) {
init {
UltronCommonConfig.testContext = testContext
}

private var beforeTest: TestMethod.() -> Unit = {}
private var afterTest: TestMethod.() -> Unit = {}
private var test: TestMethod.() -> Unit = {}

internal fun attack() {
beforeTest()
test()
afterTest()
}

fun before(block: TestMethod.() -> Unit) = apply {
beforeTest = block
}

fun after(block: TestMethod.() -> Unit) = apply {
afterTest = block
}

fun go(block: TestMethod.() -> Unit) = apply {
test = block
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.atiurin.ultron.core.test

import com.atiurin.ultron.annotations.ExperimentalUltronApi
import com.atiurin.ultron.core.test.context.DefaultUltronTestContextProvider
import com.atiurin.ultron.core.test.context.UltronTestContextProvider
import com.atiurin.ultron.exceptions.UltronException

/**
* Base class for tests in the Ultron framework. Provides mechanisms for managing test context,
* common pre-test and post-test actions, and handling test lifecycle events.
*
* @param testContextProvider Provides the context for the test. Defaults to [DefaultUltronTestContextProvider].
*/
open class UltronTest(
private val testContextProvider: UltronTestContextProvider = DefaultUltronTestContextProvider()
) {
companion object {
/**
* A map to track whether `beforeAllTests` has been executed for a given class name.
*/
private val beforeAllTestsExecutionMap: MutableMap<String, Boolean> = mutableMapOf()
}

/**
* The fully qualified name of the current test class.
*
* @throws UltronException if the test class is anonymous.
*/
private val className = this::class.qualifiedName
?: throw UltronException("Don't use anonymous class for UltronTest")

/**
* Function to be executed once before all tests in the class.
* Can be overridden in subclasses.
*/
@ExperimentalUltronApi
open val beforeFirstTest: () -> Unit = {}

/**
* Function to be executed before each test.
* Can be overridden in subclasses.
*/
open val beforeTest: () -> Unit = {}

/**
* Function to be executed after each test.
* Can be overridden in subclasses.
*/
open val afterTest: () -> Unit = {}

/**
* Executes a test with the provided configuration.
*
* @param suppressCommonBefore If `true`, the `beforeTest` function will not be executed. Defaults to `false`.
* @param suppressCommonAfter If `true`, the `afterTest` function will not be executed. Defaults to `false`.
* @param configureTestBlock The block of test logic to execute. Implemented as an extension of [TestMethod].
*/
@OptIn(ExperimentalUltronApi::class)
fun test(
suppressCommonBefore: Boolean = false,
suppressCommonAfter: Boolean = false,
configureTestBlock: TestMethod.() -> Unit
) {
TestMethod(testContextProvider.provide()).apply {
// Ensure `beforeAllTests` is executed only once per class
if (beforeAllTestsExecutionMap[className] != true) {
beforeFirstTest()
beforeAllTestsExecutionMap[className] = true
}

// Execute common `beforeTest` logic if not suppressed
if (!suppressCommonBefore) {
beforeTest()
}

// Configure and execute the test block
configureTestBlock()
attack()

// Execute common `afterTest` logic if not suppressed
if (!suppressCommonAfter) {
afterTest()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.atiurin.ultron.core.test.context

import com.atiurin.ultron.core.common.resultanalyzer.DefaultSoftAssertionOperationResultAnalyzer
import com.atiurin.ultron.core.common.resultanalyzer.OperationResultAnalyzer

open class DefaultUltronTestContext : UltronTestContext {
override var softAssertion: Boolean = false
override val softAnalyzer = DefaultSoftAssertionOperationResultAnalyzer()

override fun wrapAnalyzerIfSoftAssertion(analyzer: OperationResultAnalyzer): OperationResultAnalyzer {
return if (softAssertion) softAnalyzer.apply {
originalAnalyzer = analyzer
} else analyzer
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.atiurin.ultron.core.test.context

open class DefaultUltronTestContextProvider : UltronTestContextProvider {
override fun provide(): UltronTestContext {
return DefaultUltronTestContext()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.atiurin.ultron.core.test.context

import com.atiurin.ultron.core.common.resultanalyzer.OperationResultAnalyzer
import com.atiurin.ultron.core.common.resultanalyzer.SoftAssertionOperationResultAnalyzer

interface UltronTestContext {
var softAssertion: Boolean
val softAnalyzer: SoftAssertionOperationResultAnalyzer

fun wrapAnalyzerIfSoftAssertion(analyzer: OperationResultAnalyzer): OperationResultAnalyzer
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.atiurin.ultron.core.test.context

interface UltronTestContextProvider {
fun provide(): UltronTestContext
}
Original file line number Diff line number Diff line change
@@ -2,12 +2,10 @@ package com.atiurin.ultron.core.compose.config

import com.atiurin.ultron.core.common.Operation
import com.atiurin.ultron.core.common.OperationResult
import com.atiurin.ultron.core.common.OperationResultAnalyzer
import com.atiurin.ultron.core.common.UltronDefaultOperationResultAnalyzer
import com.atiurin.ultron.core.common.resultanalyzer.OperationResultAnalyzer
import com.atiurin.ultron.core.compose.operation.ComposeOperationResult
import com.atiurin.ultron.core.compose.operation.ComposeOperationType
import com.atiurin.ultron.core.compose.operation.UltronComposeOperation
import com.atiurin.ultron.core.config.UltronCommonConfig
import com.atiurin.ultron.exceptions.UltronAssertionException
import com.atiurin.ultron.exceptions.UltronException
import com.atiurin.ultron.exceptions.UltronWrapperException
@@ -16,20 +14,23 @@ import com.atiurin.ultron.listeners.LogLifecycleListener
import com.atiurin.ultron.listeners.UltronLifecycleListener
import com.atiurin.ultron.log.ULogger
import com.atiurin.ultron.log.UltronLog
import com.atiurin.ultron.core.config.UltronCommonConfig as config

object UltronComposeConfig {
init {
UltronCommonConfig.operationsExcludedFromListeners.addAll(
config.operationsExcludedFromListeners.addAll(
listOf(ComposeOperationType.GET_LIST_ITEM, ComposeOperationType.GET_LIST_ITEM_CHILD)
)
UltronCommonConfig.addListener(LogLifecycleListener())
config.addListener(LogLifecycleListener())
}

const val DEFAULT_LAZY_COLUMN_OPERATIONS_TIMEOUT = 10_000L

@Deprecated(
message = "Default moved to UltronCommonConfig.Defaults",
replaceWith = ReplaceWith(expression = "UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS")
)
const val DEFAULT_OPERATION_TIMEOUT = UltronCommonConfig.Defaults.OPERATION_TIMEOUT_MS
const val DEFAULT_OPERATION_TIMEOUT = config.Defaults.OPERATION_TIMEOUT_MS

var params: UltronComposeConfigParams = UltronComposeConfigParams()

@@ -45,7 +46,7 @@ object UltronComposeConfig {
@Deprecated("Use [UltronComposeConfig.params.operationTimeoutMs]")
var OPERATION_TIMEOUT = params.operationTimeoutMs

var resultAnalyzer: OperationResultAnalyzer = UltronDefaultOperationResultAnalyzer()
var resultAnalyzer: OperationResultAnalyzer = config.resultAnalyzer

inline fun setResultAnalyzer(crossinline block: (OperationResult<Operation>) -> Boolean) {
resultAnalyzer = object : OperationResultAnalyzer {
@@ -58,7 +59,7 @@ object UltronComposeConfig {
}

val resultHandler: (ComposeOperationResult<UltronComposeOperation>) -> Unit = {
resultAnalyzer.analyze(it)
config.testContext.wrapAnalyzerIfSoftAssertion(resultAnalyzer).analyze(it)
}

var allowedExceptions = mutableListOf(
@@ -74,7 +75,7 @@ object UltronComposeConfig {
)
fun addListener(listener: UltronLifecycleListener) {
UltronLog.info("Add UltronComposeOperationLifecycle listener ${listener.simpleClassName()}")
UltronCommonConfig.addListener(listener)
config.addListener(listener)
}

fun applyRecommended() {
@@ -87,12 +88,12 @@ object UltronComposeConfig {
modify()
}

private fun modify(){
private fun modify() {
getPlatformLoggers().forEach {
UltronLog.addLogger(it)
}
UltronCommonConfig.addListener(LogLifecycleListener())
if (UltronCommonConfig.logToFile) {
config.addListener(LogLifecycleListener())
if (config.logToFile) {
UltronLog.addLogger(UltronLog.fileLogger)
} else {
UltronLog.removeLogger(UltronLog.fileLogger.id)

0 comments on commit 474e8ee

Please sign in to comment.