Flaky tests are a notorious industry wide problem, especially for UI tests. Ever since powerful automated testing tools have been available, developers and testers have struggled to reduce their flakiness.
The most common solution to flakiness are quarantine (not running the tests) and Global Auto Retry -- running a test suite, group together the failures, and retry just the failures. Global Auto Retry solves the flakiness problem, but it has some disadvantages:
Instead of retrying entire tests on failure, Targeted Auto Retry focuses on retrying just the steps which are most likely to cause issues with flakiness.
In our experience, using Targeted Auto Retry with just a few common test steps is enough to eliminate 99% of flakiness from most teams' entire regression test suites.
Here's how it works:
This Targeted approach has a few advantages over the Global approach:
XCTContext.runActivity
. So relevant, actionable information about Auto Retries will be easy to find in any test reporting system, without needing to sort through multiple test run logs. These logs also provide flags set in the test steps, which can be used in test reporting solutions to pull out actionable metrics around the retry attempts:. They also support eye-catching emojis to quickly draw your attention to what's happening:♻️♻️♻️ [RETRY INFO: 2] Launch Attempt Unsuccessful. Number of attempts: 3. Attempts remaining: 1. Retrying.
♻️♻️♻️▶️ [NEW RETRY ACTION: 1] Sign In. Rerty attempt: 1. Attempts remaining 2.
♻️♻️♻️⏱ [SUCCESS CONDITION: 3] Wait for Success Condition for action: Navigate To HomePage. Retry attempt:3. Attempts remaining: 0
♻️♻️♻️⏪ [RESET STEPS: 2] Run Reset Steps to Retry action
♻️♻️♻️❌ [FAIL] Auto Retry failed 3 times for action: Search for Nike Shoes. No more retries will be attempted.
#file
and #line
macros to push information to the XCTAssert about the file and line of the test using autoRetry
, which allows any failures to be reported on the test case layer in Xcode for clarity.It's worth pointing out that it is entirely possible to run both Global Auto Retry and Targeted Auto Retry at the same time. Suppose your team has already invested in a Global Auto Retry solution that you like, and you're not looking to ditch it completely for Targeted Auto Retry, but you want to find ways to tune up your solution and make it run more efficiently. You can add Targeted Auto Retry to your tests just as easily to improve and optimize your performance.
Targeted Auto Retry is a powerful solution. By design, every use is intentional. This means that, unlike with Global Auto Retry, it's less likely that Targeted Auto Retry will accidentally gloss over bad code. However, it's still entirely capable of covering for bad / flaky code by re-trying until it works. Be sure to only use Targeted Auto Retry in cases where:
See Apple's guide to adding Package Dependencies to your app.
import TargetedAutoRetry
to a base test class file, and add the import TargetedAutoRetry
in that file at the top, next to import XCTest
. Then add the TargetedAutoRetry
protocol to your base test class. For example, if you have a base test class called class MyAwesomeBaseTestClass: XCTestCase
, add the protocol there: class MyAwesomeBaseTestClass: XCTestCase, TargetedAutoRetry
. All subclasses will inherit the same functionality without needing to import or add the protocol again.For example, instead of using XCUIApplication().launch(), replace it with:
autoRetry(
mainAction: { XCUIApplication().launch() },
successCondition: { XCUIApplication().buttons["Foo"].exists },
actionDescription: "Launch"
)
[Note: for robust results, you'll probably want to use some kind of smart wait logic for your successCondition code blocks, but the implementation is up to you]. Or for sign in, assuming there are existing signIn() , isSignedIn() -> Bool, and launch() methods:
autoRetry(
mainAction: { signIn(username, password) },
successCondition: isSignedIn,
resetSteps: launch,
actionDescription: "Sign In"
)
And that's it! You've just solved flakiness for those test steps.
[Note: For the first steps in a test, the resetSteps would likely be launch, but later on in a test, you may be able to save time by invoking steps to reset the app to just before the mainAction without starting all over from launch. This helps achieve the best performance benefits possible from this solution.]
[Note: A quick optimization in this code could be to use [weak self] for the code blocks to avoid potential memory leaks. There are a number of different ways to work with code blocks in Swift and other modern languages. The TargetedAutoRetry protocol is designed to be flexible, you can use whatever pattern works best for you.]
In our experience, using Targeted Auto Retry with just a few common test steps is enough to eliminate the vast majority of flakiness from most teams' entire regression test suites.
[Note: Targeted Auto Retry is a powerful tool. It should be used for test steps where you are confident it's not reasonably possible to remove their flakiness directly by improving the code. One of its advantages over Global Auto Retry is that it doesn't silently bandaid over flakiness caused by bad code. But it is still capable of being abused by deliberately choosing to use it to wrap over bad code.]
There are a number of implementation examples in TargetedAutoRetryTests.swift
Targeted Auto Retry comes with a few built-in configurable parameters:
retryAttempts: the number of retries to attempt before failing the test. Defaults to 3.
actionDescription: optional custom description of the test step for use in reporting. This is used in the console reporting logs. e.g. for "Launch": ♻️♻️♻️▶️ [NEW RETRY ACTION: 2] Launch. Retry attempt: 2. Attempts remaining: 1
failTestOnFailure: defaults to true, but in certain limited edge cases (such as nesting auto retry steps) it can make sense to not want to fail the test after using up all retry attempts.
That has been enough to support all the native teams at eBay up to now. But the solution is very light-weight, so it would be easy to extend it to adopt additional functionality if needed.
In some cases, you may find utility in a nested solution. This could happen if your app has multiple test steps in a row where each one is susceptible to flakiness. Imagine the following scenario: launch(), signIn(), and navToPage() are each susceptible to flakiness in your app. You wrap each one individually in autoRetry logic, but it still isn't fixing the flakiness issue for you:
func testMyAwesomeTest() {
autoRetry(
mainAction: launch,
successCondition: isLaunched,
actionDescription: "Launch"
)
autoRetry(
mainAction: signIn,
successCondition: isSignedIn,
resetSteps: launch,
actionDescription: "Sign In"
)
autoRetry(
mainAction: navToPage,
successCondition: pageLoaded,
resetSteps: navToHomePage,
actionDescription: "Navigate To Page"
)
...
//test something on the page
}
One option is to nest the autoRetries together:
func launchWithRetries() {
autoRetry(
mainAction: launch,
successCondition: isLaunched,
actionDescription: "Launch"
)
}
func signInWithRetries() {
autoRetry(
mainAction: signIn,
successCondition: isSignedIn,
resetSteps: launch,
actionDescription: "Sign In"
)
}
func navToPageWithRetries() {
autoRetry(
mainAction: navToPage,
successCondition: pageLoaded,
resetSteps: navToHomePage,
actionDescription: "Navigate To Page"
)
}
func launchSignInNavToPage() {
autoRetry(
mainAction: {
launchWithRetries()
signInWithRetries()
navToPageWithRetries()
},
successCondition: pageLoaded,
actionDescription: "Launch, Sign In, and Navigate To Page"
)
}
In this example, if one of the child-level autoRetries failed after exhausting its retry attempts, it would fail the entire test, even if the parent-level autoRetry was only on its first attempt. In order to allow the most flexibility, the child-level autoRetries could be passed the parameter failTestOnFailure: false, while the parent-level retries keep the default value of failTestOnFailure: true.
func launchWithRetries(failTestOnFailure: Bool = true) {
autoRetry(
mainAction: launch,
successCondition: isLaunched,
actionDescription: "Launch",
failTestOnFailure: failTestOnFailure
)
}
func signInWithRetries(failTestOnFailure: Bool = true) {
autoRetry(
mainAction: signIn,
successCondition: isSignedIn,
resetSteps: launch,
actionDescription: "Sign In",
failTestOnFailure: failTestOnFailure
)
}
func navToPageWithRetries(failTestOnFailure: Bool = true) {
autoRetry(
mainAction: navToPage,
successCondition: pageLoaded,
resetSteps: navToHomePage,
actionDescription: "Navigate To Page",
failTestOnFailure: failTestOnFailure
)
}
func launchSignInNavToPage() {
autoRetry(
mainAction: {
launchWithRetries(failTestOnFailure: false)
signInWithRetries(failTestOnFailure: false)
navToPageWithRetries(failTestOnFailure: false)
},
successCondition: pageLoaded,
actionDescription: "Launch, Sign In, and Navigate To Page"
)
}
From there, any tests depending on launch, signIn, and navToPage working as a precondition can just call that function, without having to think about the autoRetry logic at all. All the work going into it will be hidden from view of the test to keep the test logic clean:
func testMyAwesomeTest() {
launchSignInNavToPage()
...
//test something on the page
}
Developed by Evan Pierce.
Developing solutions for other platforms and languages is encouraged and will be shared and credited.
Copyright 2020 eBay Inc.
Developer/Architect: Evan Pierce
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
link |
Stars: 12 |
Last commit: 3 years ago |
Initial Implementation of Targeted Auto Retry. Integrated with eBay's iOS Lucid test report system (to be open sourced in Q3 2021).
Supports the following parameters:
Current supported console logging pattern examples: ♻️♻️♻️ [RETRY INFO: 2] Launch Attempt Unsuccessful. Number of attempts: 3. Attempts remaining: 1. Retrying.
♻️♻️♻️▶️ [NEW RETRY ACTION: 1] Sign In. Rerty attempt: 1. Attempts remaining 2.
♻️♻️♻️⏱ [SUCCESS CONDITION: 3] Wait for Success Condition for action: Navigate To HomePage. Retry attempt:3. Attempts remaining: 0
♻️♻️♻️⏪ [RESET STEPS: 2] Run Reset Steps to Retry action
♻️♻️♻️❌ [FAIL] Auto Retry failed 3 times for action: Search for Nike Shoes. No more retries will be attempted.
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics