DEPENDENCY_BASED Execution
Mode)If we write down rules applied to resolve what scenes and in what order they are executed in an imperative way, it will as follows:
PASSTHTHROUGH
mode is selected.DEPENDENCY_BASED mode, following rules are
respected.
@DependsOn:
beforeAll stage.beforeAll stage.@When:
@When is explicitly specified.@When annotation.@PreparedBy:
@PreparedBy annotation for the scene or all the previous
ones failed.@PreparedBy has succeeded
the scene itself will be performed. If the scene succeeds, the rest
scenes provided by @PreparedBy will not be performed.@PreparedBy scene
fails, the next one will be tried. If it is the last one, the entire
scene considered failed.@ClosedBy:
beforeAll and
beforeEach stages.beforeAll stage
succeeds, the scene specified by this annotation will be attempted in
afterAll stage.beforeEach stage, the specified scene will
be attempted in afterEach stage.Don’t get scared. This looks a bit complex but just carefully designed to model what human does a manual test. In this section we will go over the annotations mentioned in the rules above one by one with working examples.
@DependsOn: The
Simple Dependency@DependsOn used to declare regular dependencies.
- In
DEPENDENCY_BASEDmode, following rules are respected.
- For
@DependsOn:
- Implicit execution happens in
beforeAllstage.- Implicit execution happens at most only once per scene. This means when multiple scenes depend on the same single scene, the scene depended on will be executed only once.
- In implicit scene execution, the order of the explicit scenes that depend on implicit scenes is respected. That is, topological sort is applied to the dependency graph composed of the scene methods and scenes not explicit declared will be put in the beginning of
beforeAllstage.
Following is the usage of the annotation.:
@AutotestExecution(defaultExecution = @Spec(
value = "sceneMethod",
planExecutionWith = DEPENDENCY_BASED))
public class Lesson extends LessonBase {
@Named
public Scene setUpMethod() {
return Scene.begin()
.act(new Let<>("InsDog"))
.act(new Sink<>(System.out::println))
.end();
}
@DependsOn("setUpMethod")
@Named
public Scene sceneMethod() {
return Scene.begin()
.act(new Let<>("InsDog"))
.act(new Sink<>(System.out::println))
.end();
}
}Here is a note about behaviors when you specify the execution order
in @AutotestExecution, which is against the dependency
declarations by @DependsOn and others. They are not defined
as of now.
Execution Plan:
[INFO ] [2024/12/27 17:00:37.027] [main] - ----
[INFO ] [2024/12/27 17:00:37.028] [main] - Execution plan is as follows:
[INFO ] [2024/12/27 17:00:37.028] [main] - - beforeAll: [setUpMethod]
[INFO ] [2024/12/27 17:00:37.028] [main] - - beforeEach: []
[INFO ] [2024/12/27 17:00:37.028] [main] - - value: [sceneMethod]
[INFO ] [2024/12/27 17:00:37.028] [main] - - afterEach: []
[INFO ] [2024/12/27 17:00:37.028] [main] - - afterAll: []
[INFO ] [2024/12/27 17:00:37.028] [main] - ----
Action Tree: beforeAll:
[INFO ] [2024/12/27 17:00:37.035] [main] - LessonDependsOn : beforeAll: [o]setUpMethod
[INFO ] [2024/12/27 17:00:37.036] [main] - LessonDependsOn : beforeAll: +-[o:0]BEGIN[setUpMethod]@[work-id-1659515968]
[INFO ] [2024/12/27 17:00:37.036] [main] - LessonDependsOn : beforeAll: |-+-[o:0]let[InsDog][page]
[INFO ] [2024/12/27 17:00:37.036] [main] - LessonDependsOn : beforeAll: | +-[o:0]sink[page]
[INFO ] [2024/12/27 17:00:37.036] [main] - LessonDependsOn : beforeAll: +-[o:0]END[setUpMethod]
Action Tree: test:
[INFO ] [2024/12/27 17:00:37.047] [main] - LessonDependsOn : value: [o]sceneMethod
[INFO ] [2024/12/27 17:00:37.048] [main] - LessonDependsOn : value: +-[o:0]BEGIN[sceneMethod]@[work-id-1337829755]
[INFO ] [2024/12/27 17:00:37.048] [main] - LessonDependsOn : value: |-+-[o:0]let[InsDog][page]
[INFO ] [2024/12/27 17:00:37.048] [main] - LessonDependsOn : value: | +-[o:0]sink[page]
[INFO ] [2024/12/27 17:00:37.048] [main] - LessonDependsOn : value: +-[o:0]END[sceneMethod]
Here is another question. Except for a set-up method, what do we want to specify here? You should specify other scene returning methods, without which the method don’t make sense. For instance, if you have a scene, that does something on a web page, but the page is a child of some others. Without opening the child page, it doesn’t make sense. But, if we specify both of them, do we really want to specify it? It’s a valid question. You can be lazy to skip it for now. In longer term, the framework will implement other execution modes, such as “reverse order execution”. Until the day, it will not use the information.
@When: Dependency
for Assertions@When is an annotation useful for defining
“assertions”.
- For
@When:
- Implicit execution happens only when the scene mentioned by the
@Whenis explicitly specified.- Implicit execution happens right after the scene mentioned by the
@Whenannotation.
Following code shows its usage.
@AutotestExecution(defaultExecution = @Spec(
value = "performTargetFunction",
planExecutionWith = DEPENDENCY_BASED))
public class LessonWhen extends LessonBase {
@Named
public Scene performTargetFunction() {
return Scene.begin().act("...").end();
}
@Named
@When("performFunction")
public Scene thenDatabaseRecordUpdated() {
return Scene.begin().add(wasDatabaseRecordUpdated()).end();
}
}In the @AutotestExecution annotation, only
performFunction is mentioned. But the
thenDatabaseRecordUpdated will be executed along with it.
In your IDE, it will be shown as:
+ LessonWhen:
+ runTestAction:
+ [1]: performTargetFunction
+ [2]: thenDatabaseRecordWasUpdated
If you focus on the method declaration is:
@Named
@When("performFunction")
public Scene thenDatabaseRecordUpdated() {
return Scene.begin().add(wasDatabaseRecordUpdated()).end();
}This is as readable as “When performFunction, then database record was updated.”
Not only that, perhaps, you may want to write multiple assertions for a single function. That will look like in your IDE’s test run window as follows.:
+ LessonWhen:
+ runTestAction:
+ [1]: performTargetFunction
+ [2]: thenDatabaseRecordWasUpdated
+ [3]: thenWindowWasUpdated
Also, the execution plan and action tree look as follows.
Execution Plan:
[INFO ] [2024/12/27 15:45:17.221] [main] - ----
[INFO ] [2024/12/27 15:45:17.221] [main] - Execution plan is as follows:
[INFO ] [2024/12/27 15:45:17.221] [main] - - beforeAll: []
[INFO ] [2024/12/27 15:45:17.221] [main] - - beforeEach: []
[INFO ] [2024/12/27 15:45:17.222] [main] - - value: [performFunction, thenDatabaseRecordUpdated]
[INFO ] [2024/12/27 15:45:17.222] [main] - - afterEach: []
[INFO ] [2024/12/27 15:45:17.222] [main] - - afterAll: []
[INFO ] [2024/12/27 15:45:17.222] [main] - ----
Action Tree
[INFO ] [2024/12/27 15:45:17.248] [main] - LessonWhen : value: [o]performFunction
[INFO ] [2024/12/27 15:45:17.248] [main] - LessonWhen : value: +-[o:0]BEGIN[performFunction]@[work-id-519303080]
[INFO ] [2024/12/27 15:45:17.248] [main] - LessonWhen : value: |-+-[o:0]let[Hello!][page]
[INFO ] [2024/12/27 15:45:17.248] [main] - LessonWhen : value: +-[o:0]END[performFunction]
[INFO ] [2024/12/27 15:45:17.256] [main] - LessonWhen : value: [o]thenDatabaseRecordUpdated
[INFO ] [2024/12/27 15:45:17.256] [main] - LessonWhen : value: +-[o:0]BEGIN[thenDatabaseRecordUpdated]@[work-id-1552400354]
[INFO ] [2024/12/27 15:45:17.257] [main] - LessonWhen : value: |-+-[o:0]let[Database record updated!][page]
[INFO ] [2024/12/27 15:45:17.257] [main] - LessonWhen : value: +-[o:0]END[thenDatabaseRecordUpdated]
When you want to run tests, you will declare the
performFunction to be executed for sure. But you may forget
mentioning the assertion part. This mechanism prevents it from
happening.
What does this rule mean, then?
Implicit execution happens only when the scene mentioned by the
@Whenis explicitly specified.
Once performTagetFunction is defined and run as a test,
we will need to use it as a part of a preparation (arranging) step.
Because targetFunction will be reused in the
product-side.
However, in such a situation, do we want to run
thenDatabaseRecordWasUpdate? No.
Here is what the rule means. If performTargetFunction is
mentioned in the @AutotestExecution, those
thenXyz actions will be performed. But if it is not, in
other words, it is mentioned by other annotations such as
@DependsOn and it is executed because of it,
thenXyz for it won’t be performed. Why does this make
sense? Because if performTargetFunction is reused in a
preparation step, it will be mentioned by @DependsOn
annotation directly or indirectly. Not directly in
@AutotestExecution. If it is directly mentioned in
@AutotestExecution, the class is actually testing
performTargetFunction and it will be valid to run
thenXyz whose @When specifies
performTargetFunction.
@PreparedBy:
“Fallback” Dependency
- For
@PreparedBy:
- Implicit execution happens only when it is the first
@PreparedByannotation for the scene or all the previous ones failed.- After one sequence defined by
@PreparedByhas succeeded the scene itself will be performed. If the scene succeeds, the rest scenes provided by@PreparedBywill not be performed.- When the previous scene or any of
@PreparedByscene fails, the next one will be tried. If it is the last one, the entire scene considered failed.
When you write tests for a given system or a component, you will find that most of the test code consists of ” arrangement” part, which prepares preconditions to perform tests. After some while, the next thing you will realize is “arrangement” part is very time-consuming and in some cases you want to optimize it even if you are sacrificing “reliability” of tests. That is, even if you know that it is necessary to re-install OS, to make tests 100% flakiness-free, reliable, and repeatable, you cannot afford it, sometimes. This is an extreme example, but this happens every day. When you are testing Web-UI, after re-login and just moving to a homepage may cause differences in test results. Still, we don’t do log-out and log back in every time.
@PreparedBy is a mechanism to perform this sequence in a
programmatic way.
@AutotestExecution(defaultExecution = @Spec(
value = "performScenario",
planExecutionWith = DEPENDENCY_BASED))
public class LessonPreparedBy extends LessonBase {
@Named
public Scene login() {
return Scene.begin().act("...").act("...").end();
}
@Named
@PreparedBy({"toHomeScreen"})
@PreparedBy({"loadLoginSession", "toHomeScreen"})
@PreparedBy({"login", "saveLoginSession"})
public Scene isLoggedIn() {
return Scene.begin().act("...").act("...").end();
}
@Named
@DependsOn("isLoggedIn")
public Scene performScenario() {
return Scene.begin().act("...").act("...").end();
}
} This class models a test, where performScenario depends
on isLoggedIn. isLoggedIn succeeds when a test
user is actually logged in. But in modern web systems, it is becoming
more and more expensive to log in them. For instance, you may be
required to do MFA, etc. If it is a manual test, you would navigate to
the home page of the system, then conduct the next test. Even if you’ve
closed a browser tab, still the cookie may remember the session, and you
have a chance to go to the home page without a problem. Only when you
run out of a way to keep conducting tests, you will do the log in
again.
Following is an execution plan of the class.
[INFO ] [2024/12/27 16:53:17.710] [main] - ----
[INFO ] [2024/12/27 16:53:17.710] [main] - Execution plan is as follows:
[INFO ] [2024/12/27 16:53:17.710] [main] - - beforeAll: [isLoggedIn]
[INFO ] [2024/12/27 16:53:17.711] [main] - - beforeEach: []
[INFO ] [2024/12/27 16:53:17.711] [main] - - value: [performScenario]
[INFO ] [2024/12/27 16:53:17.711] [main] - - afterEach: []
[INFO ] [2024/12/27 16:53:17.711] [main] - - afterAll: []
[INFO ] [2024/12/27 16:53:17.711] [main] - ----
Only logged in is shown in the plan. Action tree looks as follows (Edited for the conciseness sake).
[INFO ] [2024/12/27 16:53:17.718] [main] - LessonPreparedBy : beforeAll: [o]isLoggedIn
[INFO ] [2024/12/27 16:53:17.718] [main] - LessonPreparedBy : beforeAll: [o:0]ensure:do sequentially using
[INFO ] [2024/12/27 16:53:17.718] [main] - LessonPreparedBy : beforeAll: |-+-[o:0]let[isLoggedIn][page]
[INFO ] [2024/12/27 16:53:17.719] [main] - LessonPreparedBy : beforeAll: | +-[o:0]sink[page]
[INFO ] [2024/12/27 16:53:17.719] [main] - LessonPreparedBy : beforeAll: +-[o:0]BEGIN[isLoggedIn]@[work-id-1636588948]
[INFO ] [2024/12/27 16:53:17.719] [main] - LessonPreparedBy : beforeAll: | |-+-[o:0]let[toHomeScreen][page]
[INFO ] [2024/12/27 16:53:17.719] [main] - LessonPreparedBy : beforeAll: +-[o:0]END[isLoggedIn]
[INFO ] [2024/12/27 16:53:17.719] [main] - LessonPreparedBy : beforeAll: +-[]BEGIN[isLoggedIn]@[work-id-662925691]
[INFO ] [2024/12/27 16:53:17.719] [main] - LessonPreparedBy : beforeAll: |-+-[]BEGIN[work-id-662925691]@[work-id-1977618945]
[INFO ] [2024/12/27 16:53:17.719] [main] - LessonPreparedBy : beforeAll: | |-+-[]let[loadLoginSession][page]
[INFO ] [2024/12/27 16:53:17.719] [main] - LessonPreparedBy : beforeAll: | +-[]END[work-id-662925691]
[INFO ] [2024/12/27 16:53:17.720] [main] - LessonPreparedBy : beforeAll: | +-[]BEGIN[work-id-662925691]@[work-id-1060519157]
[INFO ] [2024/12/27 16:53:17.720] [main] - LessonPreparedBy : beforeAll: | |-+-[]let[toHomeScreen][page]
[INFO ] [2024/12/27 16:53:17.720] [main] - LessonPreparedBy : beforeAll: | +-[]END[work-id-662925691]
[INFO ] [2024/12/27 16:53:17.720] [main] - LessonPreparedBy : beforeAll: | +-[]BEGIN[work-id-662925691]@[work-id-1060519157]
[INFO ] [2024/12/27 16:53:17.720] [main] - LessonPreparedBy : beforeAll: | |-+-[]let[login][page]
[INFO ] [2024/12/27 16:53:17.720] [main] - LessonPreparedBy : beforeAll: | | +-[]sink[page]
[INFO ] [2024/12/27 16:53:17.720] [main] - LessonPreparedBy : beforeAll: | |-+-[]let[saveLoginSession][page]
[INFO ] [2024/12/27 16:53:17.720] [main] - LessonPreparedBy : beforeAll: | +-[]END[work-id-662925691]
[INFO ] [2024/12/27 16:53:17.721] [main] - LessonPreparedBy : beforeAll: +-[]END[isLoggedIn]
Similar situation happens everywhere in testing. Unless you know data sets in your system, you don’t want to reload them, even if it is automated. Ultimately, for the reliability’s sake, it is necessary to be able to re-provision the entire system from the bare-metal operating system automatically, in theory. Yes, it is possible. However, still reusing the state which was prepared by other tests is necessary at the same time.
@PreparedBy notation and the mechanism provides a
uniformed way achieve this.
@ClosedBy: Resource
Clean-upSometimes a test consumes scarce system resources. Such resources sometimes require clean-ups. Database connection is just one example.
- For
@ClosedBy:
- This is only valid for scenes in
beforeAllandbeforeEachstages.- If a scene annotated with this in
beforeAllstage succeeds, the scene specified by this annotation will be attempted inafterAllstage.- If it is in
beforeEachstage, the specified scene will be attempted inafterEachstage.
In the context of testing, this concern happens in arrangement step
before actual tests. In InsDog’s execution model, it
means they happen beforeAll and beforeEach
stages. If a resource is allocated in beforeAll, it should
be released in afterAll. If it is beforeEach,
it should be in afterEach. If multiple resources are
allocated, they should be released in the revered order.
Following is a code example of the usage of @ClosedBy
annotation.:
@AutotestExecution(defaultExecution = @Spec(
value = "performScenario",
planExecutionWith = DEPENDENCY_BASED))
public class LessonClosedBy extends LessonBase {
@Named
@ClosedBy("closeExecutionSession")
public Scene openExecutionSession() {
return Scene.begin().act("...").act("...").end();
}
@Named
@DependsOn("openExecutionSession")
public Scene closeExecutionSession() {
return Scene.begin().act("...").act("...").end();
}
@Named
@DependsOn("openExecutionSession")
public Scene performScenario() {
return Scene.begin().act("...").act("...").end();
}
}The main entry point performScenario depends on
openExecutionSession. Therefore,
openExecutionSession will be executed in the
beforeAll stage. As openExecutionSession is
annotated with @ClosedBy("closeExecutionSession"), the
closeExecutionSession will be performed. Since
openExecutionSession is performed in
beforeAll, corresponding closing operation:
closeExecutionSession, will be performed in
afterAll1. Note that
closeExecutionSession will be performed unless the
openExecutionSession succeeds. So, it is highly recommended
to write the openExecutionSession in an “atomic” manner,
where an operation completely succeeds, otherwise it leaves no side
effect at all.
Execution Plan:
[INFO ] [2024/12/27 17:27:13.746] [main] - ----
[INFO ] [2024/12/27 17:27:13.746] [main] - Execution plan is as follows:
[INFO ] [2024/12/27 17:27:13.746] [main] - - beforeAll: [openExecutionSession]
[INFO ] [2024/12/27 17:27:13.746] [main] - - beforeEach: []
[INFO ] [2024/12/27 17:27:13.746] [main] - - value: [performScenario]
[INFO ] [2024/12/27 17:27:13.747] [main] - - afterEach: []
[INFO ] [2024/12/27 17:27:13.747] [main] - - afterAll: []
[INFO ] [2024/12/27 17:27:13.747] [main] - ----
closeExecutionSession should be executed in the
afterAll stage, but it is not shown. The reason why is,
because closeExecutionSession may not be executed in case
openExecutionSession fails. However, this is a matter of
design choice and this behavior may be modified in the future.
Action Tree (beforeAll)
Since openExecutionSession is depended on by
performScenario, it is automatically executed.
[INFO ] [2024/12/27 17:27:13.786] [main] - LessonClosedBy : beforeAll: [o]openExecutionSession
[INFO ] [2024/12/27 17:27:13.786] [main] - LessonClosedBy : beforeAll: +-[o:0]BEGIN[openExecutionSession]@[work-id-1620459733]
[INFO ] [2024/12/27 17:27:13.786] [main] - LessonClosedBy : beforeAll: |-[o:0]let[openExecutionSession][page]
[INFO ] [2024/12/27 17:27:13.786] [main] - LessonClosedBy : beforeAll: +-[o:0]END[openExecutionSession]
Action Tree (test)
The main scenario: performScenario, which is explicitly
specified in the execution directive, is performed.
[INFO ] [2024/12/27 17:27:13.811] [main] - LessonClosedBy : value: [o]performScenario
[INFO ] [2024/12/27 17:27:13.811] [main] - LessonClosedBy : value: +-[o:0]BEGIN[performScenario]@[work-id-1976166251]
[INFO ] [2024/12/27 17:27:13.811] [main] - LessonClosedBy : value: |-[o:0]let[openExecutionSession][page]
[INFO ] [2024/12/27 17:27:13.811] [main] - LessonClosedBy : value: +-[o:0]END[performScenario]
Action Tree (afterAll)
As we saw above, openExecutionSession is executed in
beforeAll stage, and it was successfully finished.
closeExecutionSession, which is specified in
@ClosedBy annotation of openExecutionSession,
is executed in the afterAll stage.
[INFO ] [2024/12/27 17:27:13.820] [main] - LessonClosedBy : afterAll: [o]closeExecutionSession
[INFO ] [2024/12/27 17:27:13.820] [main] - LessonClosedBy : afterAll: +-[o:0]BEGIN[closeExecutionSession]@[work-id-435914790]
[INFO ] [2024/12/27 17:27:13.820] [main] - LessonClosedBy : afterAll: |-[o:0]let[closeExecutionSession][page]
[INFO ] [2024/12/27 17:27:13.821] [main] - LessonClosedBy : afterAll: +-[o:0]END[closeExecutionSession]
closeExecutionSession is declared to be
depending on openExecutionSession. This is because it needs
to resolve the variable that holds a resource to be released. This
design might be changed so that closeExecutionSession
doesn’t require the explicit declaration of
@DependsOn("openExecutionSession").↩︎