Repository: junit-team/junit5 (language: java)

Issue:
Introduce `@BeforeSuite` and `@AfterSuite` annotations
I suggest that `@BeforeSuite` and `@AfterSuite` annotations should be added to JUnit 5.

Obviously they should execute before and after the whole suite.

Or is there some other mechanism to accomplish that?

I suggest that there should not be static restriction, i.e. also non-static methods could be annotated with `@BeforeSuite` and `@AfterSuite`.

I personally prefer non-static as we use Spring in our tests and want to use injected beans in `@BeforeSuite` method.



Discussion / hints:
Related to #163.

There's not always an easy way to control what's considered a "Suite".  If you look at the Maven plugin, it controls the inclusion/exclusion of tests via XML.  The `JUnitPlatform` test runner allows a suite to be defined declaratively but it's not honored by the Maven plugin.

If there was always a declarative way to run a suite, I'd say that `@BeforeAll`/`@AfterAll` could just be reused on the suite declaration.  As things stand and in the name of unambiguous behavior, I prefer your annotations.

Related to #19 
(e.g.: _@BeforeAllTests/@AfterAllTests_)

In my consideration "Suite" can be defined via:
- Maven/Gradle plugin inclusion/exclusion
- `JUnitPlatform` declarative declaration
- IDE when I click Run on the whole package of tests

In all those cases I would expect that `@BeforeSuite`/`@AfterSuite` would be executed just once before/after the whole bunch of tests, for example to perform time consuming operation such as database cleanup.

I agree with everything @ondrejlerch brings up
@ondrejlerch How would JUnit find `@BeforeSuite`/`@AfterSuite` annotated methods? For example, when you run a test class from an IDE would it only look at the test class or someplace else?

The `Store` abstraction along with the root `ExtensionContext` provides a means to prepare something once and share it between multiple test classes. However, there's no way to clean it up at the moment (that depends on #742).
@marcphilipp you could consider a solution such as Jandex, which provides you an index over all annotations
https://github.com/wildfly/jandex ?
Well, in any case, it has to be obvious for test authors where JUnit will actually look for these annotations, i.e. if they will be applied when they execute a test class.
```
environment (shell/build tool/IDE/...)  (1)
  platform/launcher (DefaultLauncher)   (2)
    engine a (Jupiter)                 (3a)
    ...
    engine x                           (3x)
    engine z                           (3z)
```

I see an easy way to implement an extension point for **3a** -- which frames the all tests executed by the Jupiter engine with a "before the first" and "after the last" callback.

Pseudo-code for [JupiterTestEngine](https://github.com/junit-team/junit5/blob/master/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java):

```java
@Override
public final void execute(ExecutionRequest request) {
	// TODO context = createExecutionContext(request);
	// try {
	// TODO callBeforeExecutionCallbacks(context);
	new HierarchicalTestExecutor<>(request, context).execute();
	// } finally {
	// TODO callAfterExecutionCallbacks(context);
	// }
}
```
For the record, I actually would not use the annotation specifically.  I am more interested in the corresponding Extension (especially AfterSuiteCallback).  

I was just answering the general question of "finding annotations".  That is what Jandex excels at, whether you want to use it or not...
I’m fine with adding an extension point that uses the existing registration mechanisms.

That should work for everything except Maven Surefire... see #1113.
As I would never use Maven ever again, I can live with that ;)

But yes, that is exactly what would work best for us:
````
@ExtendWith( MyAfterSuiteCallback.class )
...
````
Started working on a PR that introduces `BeforeExecutionCallback` and `AfterExecutionCallback`...
@marcphilipp 
To your question: How would JUnit find `@BeforeSuite`/`@AfterSuite` annotated methods? For example, when you run a test class from an IDE would it only look at the test class or someplace else?
I suggest answer: JUnit should search among all executed test classes, not someplace else. So if you execute just one test class (via IDE or Maven or Gradle) it should look only there. However, if you execute multiple test classes it should look at all of them and and run `@BeforeSuite` first.

If we use JUnit 5 with Spring then Spring Context should be initialized first, then `@BeforeSuite` should be executed, so that we can use Spring in `@BeforeSuite`. Obviously `@AfterSuite` should be executed before Spring Context is closed for the same reason.

`BeforeExecutionCallback` and `AfterExecutionCallback` sound also good.

Solution should work with IDE, Maven and Gradle as those are most typical tools for test execution. I agree with @sebersole that Maven is not cutting edge, however, if Maven Surefire would not work then please suggest what would work in Maven as it is widely used.

Thanks!
@sormuras Thanks so much for starting on this.
I'm also fine with the idea of introducing new `BeforeExecutionCallback` and `AfterExecutionCallback` extension APIs.

But... we'll have to work on the names. 😉 

That and the _implementation_ and _semantics_ of course.
Hi everybody,

I'd like to point out that we have to be very cautious here about the use of the term "suite" for a feature like this.

#744 is the intended solution for suites on the _Platform_.

Whereas, _this_ issue is about _before_ and _after_ callbacks that are specific to a given run of the Jupiter TestEngine.

So please keep this in mind so that we don't hinder the applicability and utility of whatever is eventually done for #744.

Thanks,

Sam
Given that clarification, maybe `BeforeTestEngineExecutionCallback` and `AfterTestEngineExecutionCallback`?
This [StackOverflow question](https://stackoverflow.com/q/52065409/2525313) provides an interesting use case (although there may be better solutions than `@BeforeSuite`):



> We are using Maven to run JUnit5 functional tests. Most of the tests require data to go through our entire system (a Storm topology) which could take around 1 minute. We need to send some data through and then wait 1 minute before we run the tests.
>
> We need to stage the data and then communicate keys and such to the tests.
>
> Is there a way to run a method/class to stage the data, then wait one minute, then run the rest of the tests?

I have a case quite similar to the one named by @nicolaiparlog. I work very close to some SQL and NoSQL databases and testing these entities are usually quite hard. If you are using MySQL/Postgres/Mongo you have three options:

1. Running tests agains a mocked or embedded database. This can be acceptable, but you are not testing exactly what you want
2. All developers and your CI have access to required databases (maybe local installations or a shared  remote one). In this case you must be very careful. Your tests must be prepared to be running concurrently (good look testing DDL sentences with MySQL) and they must leave, in any situation, the database on a correct state.
3. You start a new database instance each time you launch a test. You can do it with some libraries that automatically unzip and starts the database for you or you can start a Docker container with exactly what you need (including initial data, which is fantastic). You can start the database once before you run your suite or once per test method/class. The latter can be done using JUnit annotations (there are some alternatives, I have my own that [starts a docker container using Junit extensions](https://github.com/gortiz/testing-tools/tree/devel), but there are some others. The first can be done manually or with Gradle/Maven, but I would like to be able to create a JUnit extension that launches the container when the suite starts and removes it when it finishes.


Running those use-cases in single class might be an option. Then you may make use of `@BeforeAll` and friends. If the single class grows too large, well, you may split it up and care about starting your "setup" only once.
I'm running these cases on a single class right now, but once you want to split the class, before/after suite could really simplify the code (and I can distribute a JUnit plugin so my coworkers can easily reuse my code on their projects).

BTW: A modern machine can start an empty MySQL/Postgres/Mongo container in a couple of seconds, so it is not really bad to start one per class. But the faster it goes, the more frequently you can run our mini-integration tests.
Keep in mind that an extension that implements `BeforeAllCallback` and is registered globally via the `ServiceLoader` mechanism can make use of `CloseableResource` and the `ExtentionContext.Store` to achieve many suite-like behaviors.

See #1555 and the discussion on Gitter linked from there for further details.
Did you plan to introduce before/after test run annotation in JUni5?
hi All!

Instead of @BeforeSute I used like this:

Add to your Base abstract class (I mean abstract class where you initialize your driver in *setUpDriver()* method) this part of code:

    private static boolean started = false;
    static{
        if (!started) {
            started = true;
            try {
                setUpDriver();  //method where you initialize your driver
            } catch (MalformedURLException e) {
            }
        }
    }

And now, if your test classes will **extends** from Base abstract class -> *setUpDriver()* method will be executed before first @Test only **ONE** time per project run.
@SergiiNik Nice suggestion! However AFAICT, it only works when running tests one after another (which is the default behaviour) instead of in parallel.

By my understanding, when running in parallel, the code `setUpDriver()` may get called multiple times before `started = true` becomes visible to all threads running the tests, so the [double-checked locking pattern](https://wiki.sei.cmu.edu/confluence/plugins/servlet/mobile?contentId=88487915#content/view/88487915) with a `Lock` may be needed in this case. :)
...or even better, the [Initialization-on-demand holder idiom](https://en.m.wikipedia.org/wiki/Initialization-on-demand_holder_idiom).
...although, I think the holder idiom only works if `setUpDriver()` returns something... 🤔
> @SergiiNik Nice suggestion! However AFAICT, it only works when running tests one after another (which is the default behaviour) instead of in parallel.
> 
> By my understanding, when running in parallel, the code `setUpDriver()` may get called multiple times before `started = true` becomes visible to all threads running the tests, so the [double-checked locking pattern](https://wiki.sei.cmu.edu/confluence/plugins/servlet/mobile?contentId=88487915#content/view/88487915) with a `Lock` may be needed in this case. :)

Hello @jbduncan , we have our own almost similar with @SergiiNik solution and it works perfect in parallel, we use the following Junit parameters:
`junit.jupiter.execution.parallel.enabled=true`
`junit.jupiter.execution.parallel.mode.default=concurrent`
`junit.jupiter.testinstance.lifecycle.default=per_class`
See my gist: 
https://gist.github.com/Legionivo/23313f5998284e25af9a4eb6570a6360
Hi guys! Do we have a chance to get  `beforeSuite/afterSuite` feature done in the near future? Or for some reason, it is postponed?
Hi all, I want to echo the curiosity around this potential feature as well. Any word on whether this is something on the roadmap?
Not on the current roadmap. There's a workaround using `ClosableResource`, though (see https://github.com/junit-team/junit5/issues/456#issuecomment-416945159).
> Keep in mind that an extension that implements `BeforeAllCallback` and is registered globally via the `ServiceLoader` mechanism can make use of `CloseableResource` and the `ExtentionContext.Store` to achieve many suite-like behaviors.
> 
> See #1555 and the discussion on Gitter linked from there for further details.

This is great, very much appreciate the hint. It has a few drawbacks though: 

  - every test goes via that extension. yes, we do filter them by some annotation obviously, but you are still paying for a conditional check in every _other_ test.

 - as soon as you need more then 3 (imho) different `Before/After`Suite, that extension becomes cumbersome to maintain (this has hit us).

 - if tests are run in parallel and in `BeforeAllCallback` there has to be a single thing done (think an embedded DB started only once): there has to be a `volatile` check against the fact _IFF_ the DB was already started. Add 3 different logical test suites needed and there are 3 volatile reads done.

These are the ones I found while implementing this pattern. 
If you use `Store.getOrComputeIfAbsent` it will do the check for you in a thread-safe manner.
Hi Team, would it be fine to ask status on implementing Before/After Test Suite annotations in Junit5? I had followed all above instructions to do (workaround), however, nothing works in my case. 

Problem Statement: We have a requirement to generate ExtentReport for our all tests in single report. Using above all suggestions, was able to generate class level report. Apart from, our tests (test classes) need some set of pre-test data to execute and flush out post run all test classes. 

If someone has any working solution, kindly share. 

Thank you. 
@praveenreddynarala Could you please share a few more details about what you tried? For creating a report, we generally recommend using a `TestExecutionListener` instead.
@marcphilipp please find the way I have created Extent Report in our project..

Step 1: We have create one TestCallback class which implements BeforeAllCallback, AfterAllCallback, Before and AfterTestExecutionCallback
Step 2: In beforeAll Callback method, I am creating ExtentReport instance
Step 3: In beforeTestExecution Callback method, I am actually creating a test ExtentReport.createTest(SampleTestMethod)
Step 4: afterTestExecution Callback method, logging all test results
Step 5: afterAll Callback method, flushing the Extent report

With above steps, it is always creating Extent Report per Test Class. I had looked at thread (https://github.com/junit-team/junit5/issues/1555) and created custom extension to create Extent report instance only once (before test classes) using ExtensionContext.Store.CloseableResource. But it does not work when I run tests in parallel using Maven plugin. 

Would it be good to know, is there a way I can initialise Extent Report before all Tests run and flush the report after all tests run. 

Not sure/aware of using TestExecutionListener and what it does. How would I integrate Extent Report using this. 

Please let me know if you would like to know any more information in regards.
> But it does not work when I run tests in parallel using Maven plugin.

There's nothing we can do here to solve that since Surefire spins up multiple VMs for that. You'd have to write a report per VM (maybe using `${surefire.forkNumber}` as part of the file name) and create a Maven goal or some other post-processing step to merge them if you need a single report.
>marcphilipp : How would JUnit find @BeforeSuite/@AfterSuite annotated methods? For example, when you run a test class from an IDE would it only look at the test class or someplace else?

@marcphilipp , @sormuras , what do you think of discovering the suite as a transitive closure of the extension links?

For instance:

```java
@ParticipatesInSuite("com.example.virtualsuite.name")
@ParticipatesInSuite(SuiteClass.class)
class VerifyE2E_TestCase1_... {
// test methods go here
}
``` 

```java
@ParticipatesInSuite("com.example.virtualsuite.name") // e.g. just a symbolic name
@ParticipatesInSuite(E2ESuiteClass.class) // so the suite class could be autocompleted, and annotations could be placed there
class VerifyE2E_TestCase2_... {
// test methods go here
}
``` 

Then:

1) When the user launches `./gradlew test`, Gradle would pass all test classes to JUnit. JUnit would find `@ParticipatesInSuite`  and it would group tests into suites
2) When the user launches a single test from IDE, IDE would pass test class/method name to JUnit, it would find `@ParticipatesInSuite` annotation, and create a suite with a single class only
3) At no point the user can forget to add a test to an XML suite or something like that. The problem with suites that "enumerate test classes" is the users can add a test and forget to list it in the suite. Then JUnit won't execute the test, and everybody would think that is fine.

I believe that would be a better superset of the current "static suites" feature, and it seems to be very well in line with the current use of `@ExtendWith` when every test class declares its dependencies and features rather than something external assigns features to test classes.

---

The key value-added here is that the users could batch data population to the system under test which is essential in E2E testing scenarios when there's "per upload" overhead, so it might be much faster to gather all the required data from all the classes and upload it once in "before suite" rather than perform small uploads for each test.

---

The suggestion does not cover the case when the same set of classes needs to be executed multiple times with different settings. For instance "install Windows VM and launch all tests against it", then "install Linux and launch all tests against it". It might need something like `SuiteFactory` or it might be covered by `E2ESuiteClass` returning multiple "suite objects"
**Team decision:** Investigate the feasibility of adding `@BeforeSuite` and `@AfterSuite` support in `@Suite` classes.
Hi @marcphilipp , 

Please feedback what is the alternative of BeforeSuite and AfterSuite , in order to create 1 consolidated Extend Report across all Test Classes . 
Same Issue Described by @praveenreddynarala 

Regards ,
Yomna Zidan 

@YomnaZidan @praveenreddynarala I am facing same issue as you both with extent report generation. Did you guys find any alternatives, if so can you please share. That would be a great help. Thanks 🙏 
@kishkk84 @YomnaZidan you can take a look at  [my solution](https://github.com/Furrial/junit5-extentreports-example/blob/main/src/main/java/com/example/reporter/TestReporter.java) implemented based on workaround using ClosableResource (see [456 comment](https://github.com/junit-team/junit5/issues/456#issuecomment-416945159))
We have the same requirement. In JUnit 4, we were using an `ExternalResource` in a class that was running with a custom `Suite` implementation in order to set up a database structure for all tests that were going to be executed in the suite and to clean up the database afterwards.
Knowing which classes are going to be executed by the Suite when these `BeforeSuite` and `AfterSuite` hooks are called would be pretty important to us, since we ask each of the executed test classes about what database contents the test requires. This allows us to set up the database just once in the beginning, instead of individually for each test, which results in a lot of saved time if multiple test classes have the same database requirements.
> Related to #163.
> 
> There's not always an easy way to control what's considered a "Suite". If you look at the Maven plugin, it controls the inclusion/exclusion of tests via XML. The `JUnitPlatform` test runner allows a suite to be defined declaratively but it's not honored by the Maven plugin.
> 
> If there was always a declarative way to run a suite, I'd say that `@BeforeAll`/`@AfterAll` could just be reused on the suite declaration. As things stand and in the name of unambiguous behavior, I prefer your annotations.

Can we not follow what the definition of suite is as per TestNG.. https://testng.org/doc/documentation-main.html

@BeforeSuite
@AfterSuite

@marcphilipp Is there any update to this?
I'm using Junit 5 in combo with Cucumber and Spring Boot.
My Suite class looks like this:

```
@Suite
@SelectPackages("features")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "stepdefs")
public class FrontofficeSuiteRunner {

}
```

I've tried a solution of creating a custom extension class with BeforeAllCallback and then extending my suite with it, but no success.
I'm finding no workaround that works for my use case.

The Suite runs all feature files in my features package and I need the BeforeAll method to do database cleanup before running the features..
@MaudeVanhengel What we endet up doing is a
- spring bean 
- with @PostConstruct and @PreDestroy methods for the BeforeSuite- and AfterSuite code
- has a test profile

We add the profile to the test execution and write our "BeforeAllCallback"-Code in the annoated methods. You could also use the InitializingBean or listen for spring application events (e.g. ContextStartedEvent) and execute code once the context is fully started. 

The problem with the Callbacks is, that you don't have access to the spring application context to for example initialize the database using spring repositories. 

WARNING: 
This solutions does **not** exactly the same as the here proposed BeforeSuite and AfterSuite annotations. The code in the bean is executed every time, the application context gets created/destroyed. So if for example a test method is annotated with springs @DirtiesContext annotation, the code will be executed again. In our case, that is actually exactly what we want. But it might not be the correct thing in your case. Also mocking the context with @MockBean will destroy/re-create the context. So use this with caution. 

Fix patch:
diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-RC1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-RC1.adoc
index e76d8c4020aa..50492c9818c3 100644
--- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-RC1.adoc
+++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-RC1.adoc
@@ -33,6 +33,7 @@ repository on GitHub.
   exception's _root_ cause matches all supplied conditions, for use with the
   `EngineTestKit`.
 * `ReflectionSupport` now supports scanning for classpath resources.
+* Introduce `@BeforeSuite` and `@AfterSuite` annotations.
 
 
 [[release-notes-5.11.0-RC1-junit-jupiter]]
diff --git a/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-suite-engine.adoc b/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-suite-engine.adoc
index d38a312d799f..ebb1d6495b04 100644
--- a/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-suite-engine.adoc
+++ b/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-suite-engine.adoc
@@ -48,3 +48,14 @@ include::{testDir}/example/SuiteDemo.java[tags=user_guide]
 NOTE: There are numerous configuration options for discovering and filtering tests in a
 test suite. Please consult the Javadoc of the `{suite-api-package}` package for a full
 list of supported annotations and further details.
+
+==== @BeforeSuite and @AfterSuite
+
+`@BeforeSuite` and `@AfterSuite` annotations can be used on methods inside a
+`@Suite`-annotated class. They will be executed respectively before and after
+all tests of the test suite.
+
+[source,java,indent=0]
+----
+include::{testDir}/example/BeforeAndAfterSuiteDemo.java[tags=user_guide]
+----
diff --git a/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/AfterSuite.java b/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/AfterSuite.java
new file mode 100644
index 000000000000..c2b5e33a3467
--- /dev/null
+++ b/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/AfterSuite.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2015-2024 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.platform.suite.api;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apiguardian.api.API;
+
+/**
+ * {@code @AfterSuite} is used to signal that the annotated method should be
+ * executed <em>after</em> <strong>all</strong> tests in the current test suite.
+ *
+ * <h2>Method Signatures</h2>
+ *
+ * <p>{@code @AfterSuite} methods must have a {@code void} return type, must
+ * be {@code static} and must not be {@code private}.
+ *
+ * <h2>Inheritance and Execution Order</h2>
+ *
+ * <p>{@code @AfterSuite} methods are inherited from superclasses as long as they
+ * are not <em>overridden</em> according to the visibility rules of the Java
+ * language. Furthermore, {@code @AfterSuite} methods from superclasses will be
+ * executed after {@code @AfterSuite} methods in subclasses.
+ *
+ * <p>The JUnit Platform Suite Engine does not guarantee the execution order of
+ * multiple {@code @AfterSuite} methods that are declared within a single test
+ * class or test interface. While it may at times appear that these methods are
+ * invoked in alphabetical order, they are in fact sorted using an algorithm
+ * that is deterministic but intentionally non-obvious.
+ *
+ * <p>In addition, {@code @AfterSuite} methods are in no way linked to
+ * {@code @BeforeSuite} methods. Consequently, there are no guarantees with regard
+ * to their <em>wrapping</em> behavior. For example, given two
+ * {@code @BeforeSuite} methods {@code createA()} and {@code createB()} as well as
+ * two {@code @AfterSuite} methods {@code destroyA()} and {@code destroyB()}, the
+ * order in which the {@code @BeforeSuite} methods are executed (e.g.
+ * {@code createA()} before {@code createB()}) does not imply any order for the
+ * seemingly corresponding {@code @AfterSuite} methods. In other words,
+ * {@code destroyA()} might be called before <em>or</em> after
+ * {@code destroyB()}. The JUnit Team therefore recommends that developers
+ * declare at most one {@code @BeforeSuite} method and at most one
+ * {@code @AfterSuite} method per test class or test interface unless there are no
+ * dependencies between the {@code @BeforeSuite} methods or between the
+ * {@code @AfterSuite} methods.
+ *
+ * <h2>Composition</h2>
+ *
+ * <p>{@code @AfterSuite} may be used as a meta-annotation in order to create
+ * a custom <em>composed annotation</em> that inherits the semantics of
+ * {@code @AfterSuite}.
+ *
+ * @since 1.11
+ * @see BeforeSuite
+ * @see Suite
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@API(status = EXPERIMENTAL, since = "1.11")
+public @interface AfterSuite {
+}
diff --git a/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/BeforeSuite.java b/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/BeforeSuite.java
new file mode 100644
index 000000000000..5b4c0d3c93f0
--- /dev/null
+++ b/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/BeforeSuite.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2015-2024 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.platform.suite.api;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apiguardian.api.API;
+
+/**
+ * {@code @BeforeSuite} is used to signal that the annotated method should be
+ * executed <em>before</em> <strong>all</strong> tests in the current test suite.
+ *
+ * <h2>Method Signatures</h2>
+ *
+ * <p>{@code @BeforeSuite} methods must have a {@code void} return type, must
+ * be {@code static} and must not be {@code private}.
+ *
+ * <h2>Inheritance and Execution Order</h2>
+ *
+ * <p>{@code @BeforeSuite} methods are inherited from superclasses as long as they
+ * are not <em>overridden</em> according to the visibility rules of the Java
+ * language. Furthermore, {@code @BeforeSuite} methods from superclasses will be
+ * executed before {@code @BeforeSuite} methods in subclasses.
+ *
+ * <p>The JUnit Platform Suite Engine does not guarantee the execution order of
+ * multiple {@code @BeforeSuite} methods that are declared within a single test
+ * class or test interface. While it may at times appear that these methods are
+ * invoked in alphabetical order, they are in fact sorted using an algorithm
+ * that is deterministic but intentionally non-obvious.
+ *
+ * <p>In addition, {@code @BeforeSuite} methods are in no way linked to
+ * {@code @AfterSuite} methods. Consequently, there are no guarantees with regard
+ * to their <em>wrapping</em> behavior. For example, given two
+ * {@code @BeforeSuite} methods {@code createA()} and {@code createB()} as well as
+ * two {@code @AfterSuite} methods {@code destroyA()} and {@code destroyB()}, the
+ * order in which the {@code @BeforeSuite} methods are executed (e.g.
+ * {@code createA()} before {@code createB()}) does not imply any order for the
+ * seemingly corresponding {@code @AfterSuite} methods. In other words,
+ * {@code destroyA()} might be called before <em>or</em> after
+ * {@code destroyB()}. The JUnit Team therefore recommends that developers
+ * declare at most one {@code @BeforeSuite} method and at most one
+ * {@code @AfterSuite} method per test class or test interface unless there are no
+ * dependencies between the {@code @BeforeSuite} methods or between the
+ * {@code @AfterSuite} methods.
+ *
+ * <h2>Composition</h2>
+ *
+ * <p>{@code @BeforeSuite} may be used as a meta-annotation in order to create
+ * a custom <em>composed annotation</em> that inherits the semantics of
+ * {@code @BeforeSuite}.
+ *
+ * @since 1.11
+ * @see AfterSuite
+ * @see Suite
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@API(status = EXPERIMENTAL, since = "1.11")
+public @interface BeforeSuite {
+}
diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/LifecycleMethodUtils.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/LifecycleMethodUtils.java
new file mode 100644
index 000000000000..b00bca64a131
--- /dev/null
+++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/LifecycleMethodUtils.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2015-2024 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.platform.suite.engine;
+
+import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedMethods;
+import static org.junit.platform.commons.util.ReflectionUtils.returnsPrimitiveVoid;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.List;
+
+import org.junit.platform.commons.JUnitException;
+import org.junit.platform.commons.util.ReflectionUtils;
+import org.junit.platform.commons.util.ReflectionUtils.HierarchyTraversalMode;
+import org.junit.platform.engine.support.hierarchical.ThrowableCollector;
+import org.junit.platform.suite.api.AfterSuite;
+import org.junit.platform.suite.api.BeforeSuite;
+
+/**
+ * Collection of utilities for working with test lifecycle methods.
+ *
+ * @since 1.11
+ */
+final class LifecycleMethodUtils {
+
+	private LifecycleMethodUtils() {
+		/* no-op */
+	}
+
+	static List<Method> findBeforeSuiteMethods(Class<?> testClass, ThrowableCollector throwableCollector) {
+		return findMethodsAndAssertStaticAndNonPrivate(testClass, BeforeSuite.class, HierarchyTraversalMode.TOP_DOWN,
+			throwableCollector);
+	}
+
+	static List<Method> findAfterSuiteMethods(Class<?> testClass, ThrowableCollector throwableCollector) {
+		return findMethodsAndAssertStaticAndNonPrivate(testClass, AfterSuite.class, HierarchyTraversalMode.BOTTOM_UP,
+			throwableCollector);
+	}
+
+	private static List<Method> findMethodsAndAssertStaticAndNonPrivate(Class<?> testClass,
+			Class<? extends Annotation> annotationType, HierarchyTraversalMode traversalMode,
+			ThrowableCollector throwableCollector) {
+
+		List<Method> methods = findAnnotatedMethods(testClass, annotationType, traversalMode);
+		throwableCollector.execute(() -> methods.forEach(method -> {
+			assertVoid(annotationType, method);
+			assertStatic(annotationType, method);
+			assertNonPrivate(annotationType, method);
+			assertNoParameters(annotationType, method);
+		}));
+		return methods;
+	}
+
+	private static void assertStatic(Class<? extends Annotation> annotationType, Method method) {
+		if (ReflectionUtils.isNotStatic(method)) {
+			throw new JUnitException(String.format("@%s method '%s' must be static.", annotationType.getSimpleName(),
+				method.toGenericString()));
+		}
+	}
+
+	private static void assertNonPrivate(Class<? extends Annotation> annotationType, Method method) {
+		if (ReflectionUtils.isPrivate(method)) {
+			throw new JUnitException(String.format("@%s method '%s' must not be private.",
+				annotationType.getSimpleName(), method.toGenericString()));
+		}
+	}
+
+	private static void assertVoid(Class<? extends Annotation> annotationType, Method method) {
+		if (!returnsPrimitiveVoid(method)) {
+			throw new JUnitException(String.format("@%s method '%s' must not return a value.",
+				annotationType.getSimpleName(), method.toGenericString()));
+		}
+	}
+
+	private static void assertNoParameters(Class<? extends Annotation> annotationType, Method method) {
+		if (method.getParameterCount() > 0) {
+			throw new JUnitException(String.format("@%s method '%s' must not accept parameters.",
+				annotationType.getSimpleName(), method.toGenericString()));
+		}
+	}
+
+}
diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java
index 9fa107cb5f2c..ccf40405f7bd 100644
--- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java
+++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java
@@ -13,8 +13,12 @@
 import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation;
 import static org.junit.platform.suite.commons.SuiteLauncherDiscoveryRequestBuilder.request;
 
+import java.lang.reflect.Method;
+import java.util.List;
+
 import org.junit.platform.commons.JUnitException;
 import org.junit.platform.commons.util.Preconditions;
+import org.junit.platform.commons.util.ReflectionUtils;
 import org.junit.platform.commons.util.StringUtils;
 import org.junit.platform.engine.ConfigurationParameters;
 import org.junit.platform.engine.EngineExecutionListener;
@@ -24,6 +28,8 @@
 import org.junit.platform.engine.discovery.DiscoverySelectors;
 import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor;
 import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.engine.support.hierarchical.OpenTest4JAwareThrowableCollector;
+import org.junit.platform.engine.support.hierarchical.ThrowableCollector;
 import org.junit.platform.launcher.LauncherDiscoveryRequest;
 import org.junit.platform.launcher.core.LauncherDiscoveryResult;
 import org.junit.platform.launcher.listeners.TestExecutionSummary;
@@ -121,16 +127,58 @@ private static String getSuiteDisplayName(Class<?> testClass) {
 
 	void execute(EngineExecutionListener parentEngineExecutionListener) {
 		parentEngineExecutionListener.executionStarted(this);
+		ThrowableCollector throwableCollector = new OpenTest4JAwareThrowableCollector();
+
+		List<Method> beforeSuiteMethods = LifecycleMethodUtils.findBeforeSuiteMethods(suiteClass, throwableCollector);
+		List<Method> afterSuiteMethods = LifecycleMethodUtils.findAfterSuiteMethods(suiteClass, throwableCollector);
+
+		executeBeforeSuiteMethods(beforeSuiteMethods, throwableCollector);
+
+		TestExecutionSummary summary = executeTests(parentEngineExecutionListener, throwableCollector);
+
+		executeAfterSuiteMethods(afterSuiteMethods, throwableCollector);
+
+		TestExecutionResult testExecutionResult = computeTestExecutionResult(summary, throwableCollector);
+		parentEngineExecutionListener.executionFinished(this, testExecutionResult);
+	}
+
+	private void executeBeforeSuiteMethods(List<Method> beforeSuiteMethods, ThrowableCollector throwableCollector) {
+		if (throwableCollector.isNotEmpty()) {
+			return;
+		}
+		for (Method beforeSuiteMethod : beforeSuiteMethods) {
+			throwableCollector.execute(() -> ReflectionUtils.invokeMethod(beforeSuiteMethod, null));
+			if (throwableCollector.isNotEmpty()) {
+				return;
+			}
+		}
+	}
+
+	private TestExecutionSummary executeTests(EngineExecutionListener parentEngineExecutionListener,
+			ThrowableCollector throwableCollector) {
+		if (throwableCollector.isNotEmpty()) {
+			return null;
+		}
+
 		// #2838: The discovery result from a suite may have been filtered by
 		// post discovery filters from the launcher. The discovery result should
 		// be pruned accordingly.
 		LauncherDiscoveryResult discoveryResult = this.launcherDiscoveryResult.withRetainedEngines(
 			getChildren()::contains);
-		TestExecutionSummary summary = launcher.execute(discoveryResult, parentEngineExecutionListener);
-		parentEngineExecutionListener.executionFinished(this, computeTestExecutionResult(summary));
+		return launcher.execute(discoveryResult, parentEngineExecutionListener);
 	}
 
-	private TestExecutionResult computeTestExecutionResult(TestExecutionSummary summary) {
+	private void executeAfterSuiteMethods(List<Method> afterSuiteMethods, ThrowableCollector throwableCollector) {
+		for (Method afterSuiteMethod : afterSuiteMethods) {
+			throwableCollector.execute(() -> ReflectionUtils.invokeMethod(afterSuiteMethod, null));
+		}
+	}
+
+	private TestExecutionResult computeTestExecutionResult(TestExecutionSummary summary,
+			ThrowableCollector throwableCollector) {
+		if (throwableCollector.isNotEmpty()) {
+			return TestExecutionResult.failed(throwableCollector.getThrowable());
+		}
 		if (failIfNoTests && summary.getTestsFoundCount() == 0) {
 			return TestExecutionResult.failed(new NoTestsDiscoveredException(suiteClass));
 		}


Test patch:
diff --git a/documentation/src/test/java/example/BeforeAndAfterSuiteDemo.java b/documentation/src/test/java/example/BeforeAndAfterSuiteDemo.java
new file mode 100644
index 000000000000..c461417361bc
--- /dev/null
+++ b/documentation/src/test/java/example/BeforeAndAfterSuiteDemo.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2015-2024 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package example;
+
+import org.junit.platform.suite.api.AfterSuite;
+import org.junit.platform.suite.api.BeforeSuite;
+import org.junit.platform.suite.api.SelectPackages;
+import org.junit.platform.suite.api.Suite;
+
+//tag::user_guide[]
+@Suite
+@SelectPackages("example")
+class BeforeAndAfterSuiteDemo {
+
+	@BeforeSuite
+	static void beforeSuite() {
+		// executes before the test suite
+	}
+
+	@AfterSuite
+	static void afterSuite() {
+		// executes after the test suite
+	}
+
+}
+//end::user_guide[]
diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/BeforeAndAfterSuiteTests.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/BeforeAndAfterSuiteTests.java
new file mode 100644
index 000000000000..c892eee146bb
--- /dev/null
+++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/BeforeAndAfterSuiteTests.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2015-2024 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.platform.suite.engine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Named.named;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+import static org.junit.platform.suite.engine.SuiteEngineDescriptor.ENGINE_ID;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.FailingAfterSuite;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.FailingBeforeAndAfterSuite;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.FailingBeforeSuite;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.NonStaticAfterSuite;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.NonStaticBeforeSuite;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.NonVoidAfterSuite;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.NonVoidBeforeSuite;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.ParameterAcceptingAfterSuite;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.ParameterAcceptingBeforeSuite;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.PrivateAfterSuite;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.PrivateBeforeSuite;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.SeveralFailingBeforeAndAfterSuite;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.SubclassWithBeforeAndAfterSuite;
+import static org.junit.platform.suite.engine.testsuites.LifecycleMethodsSuites.SuccessfulBeforeAndAfterSuite;
+import static org.junit.platform.testkit.engine.EventConditions.container;
+import static org.junit.platform.testkit.engine.EventConditions.event;
+import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully;
+import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
+import static org.junit.platform.testkit.engine.EventConditions.test;
+import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
+import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
+import static org.junit.platform.testkit.engine.TestExecutionResultConditions.suppressed;
+
+import java.util.ArrayList;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.platform.commons.JUnitException;
+import org.junit.platform.suite.api.AfterSuite;
+import org.junit.platform.suite.api.BeforeSuite;
+import org.junit.platform.suite.engine.testcases.StatefulTestCase;
+import org.junit.platform.testkit.engine.EngineExecutionResults;
+import org.junit.platform.testkit.engine.EngineTestKit;
+
+/**
+ * Integration tests that verify support for {@link BeforeSuite} and {@link AfterSuite},
+ * in the {@link SuiteTestEngine}.
+ *
+ * @since 1.11
+ */
+public class BeforeAndAfterSuiteTests {
+
+	@BeforeEach
+	void setUp() {
+		StatefulTestCase.callSequence = new ArrayList<>();
+	}
+
+	@Test
+	void successfulBeforeAndAfterSuite() {
+		// @formatter:off
+		executeSuite(SuccessfulBeforeAndAfterSuite.class)
+				.allEvents()
+				.assertStatistics(stats -> stats.started(7).finished(7).succeeded(6).failed(1))
+				.assertThatEvents()
+				.haveExactly(1, event(test(StatefulTestCase.Test1.class.getName()), finishedSuccessfully()))
+				.haveExactly(1, event(test(StatefulTestCase.Test2.class.getName()), finishedWithFailure()));
+
+		assertThat(StatefulTestCase.callSequence).containsExactly(
+				"beforeSuiteMethod",
+				"test1",
+				"test2",
+				"afterSuiteMethod"
+		);
+		// @formatter:on
+	}
+
+	@Test
+	void beforeAndAfterSuiteInheritance() {
+		// @formatter:off
+		executeSuite(SubclassWithBeforeAndAfterSuite.class)
+				.allEvents()
+				.assertStatistics(stats -> stats.started(7).finished(7).succeeded(6).failed(1));
+
+		assertThat(StatefulTestCase.callSequence).containsExactly(
+				"superclassBeforeSuiteMethod",
+				"subclassBeforeSuiteMethod",
+				"test1",
+				"test2",
+				"subclassAfterSuiteMethod",
+				"superclassAfterSuiteMethod"
+		);
+		// @formatter:on
+	}
+
+	@Test
+	void failingBeforeSuite() {
+		// @formatter:off
+		executeSuite(FailingBeforeSuite.class)
+				.allEvents()
+				.assertStatistics(stats -> stats.started(2).finished(2).succeeded(1).failed(1))
+				.assertThatEvents()
+				.haveExactly(1, event(
+						container(FailingBeforeSuite.class),
+						finishedWithFailure(instanceOf(RuntimeException.class),
+								message("Exception thrown by @BeforeSuite method"))));
+
+		assertThat(StatefulTestCase.callSequence).containsExactly(
+				"beforeSuiteMethod",
+				"afterSuiteMethod"
+		);
+		// @formatter:on
+	}
+
+	@Test
+	void failingAfterSuite() {
+		// @formatter:off
+		executeSuite(FailingAfterSuite.class)
+				.allEvents()
+				.assertStatistics(stats -> stats.started(7).finished(7).succeeded(5).failed(2))
+				.assertThatEvents()
+				.haveExactly(1, event(
+						container(FailingAfterSuite.class),
+						finishedWithFailure(instanceOf(RuntimeException.class),
+								message("Exception thrown by @AfterSuite method"))));
+
+		assertThat(StatefulTestCase.callSequence).containsExactly(
+				"beforeSuiteMethod",
+				"test1",
+				"test2",
+				"afterSuiteMethod"
+		);
+		// @formatter:on
+	}
+
+	@Test
+	void failingBeforeAndAfterSuite() {
+		// @formatter:off
+		executeSuite(FailingBeforeAndAfterSuite.class)
+				.allEvents()
+				.assertStatistics(stats -> stats.started(2).finished(2).succeeded(1).failed(1))
+				.assertThatEvents()
+				.haveExactly(1, event(
+						container(FailingBeforeAndAfterSuite.class),
+						finishedWithFailure(instanceOf(RuntimeException.class),
+								message("Exception thrown by @BeforeSuite method"),
+								suppressed(0, instanceOf(RuntimeException.class),
+										message("Exception thrown by @AfterSuite method")))));
+
+		assertThat(StatefulTestCase.callSequence).containsExactly(
+				"beforeSuiteMethod",
+				"afterSuiteMethod"
+		);
+		// @formatter:on
+	}
+
+	@Test
+	void severalFailingBeforeAndAfterSuite() {
+		// @formatter:off
+		executeSuite(SeveralFailingBeforeAndAfterSuite.class)
+				.allEvents()
+				.assertStatistics(stats -> stats.started(2).finished(2).succeeded(1).failed(1))
+				.assertThatEvents()
+				.haveExactly(1, event(
+						container(SeveralFailingBeforeAndAfterSuite.class),
+						finishedWithFailure(instanceOf(RuntimeException.class),
+								message("Exception thrown by @BeforeSuite method"),
+								suppressed(0, instanceOf(RuntimeException.class),
+										message("Exception thrown by @AfterSuite method")),
+								suppressed(1, instanceOf(RuntimeException.class),
+										message("Exception thrown by @AfterSuite method")))));
+
+		assertThat(StatefulTestCase.callSequence).containsExactly(
+				"beforeSuiteMethod",
+				"afterSuiteMethod",
+				"afterSuiteMethod"
+		);
+		// @formatter:on
+	}
+
+	@ParameterizedTest(name = "{0}")
+	@MethodSource
+	void invalidBeforeOrAfterSuiteMethod(Class<?> testSuiteClass, Predicate<String> failureMessagePredicate) {
+		// @formatter:off
+		executeSuite(testSuiteClass)
+				.allEvents()
+				.assertThatEvents()
+				.haveExactly(1, event(
+						container(testSuiteClass),
+						finishedWithFailure(instanceOf(JUnitException.class), message(failureMessagePredicate))));
+		// @formatter:on
+	}
+
+	private static Stream<Arguments> invalidBeforeOrAfterSuiteMethod() {
+		return Stream.of(
+			invalidBeforeOrAfterSuiteCase(NonVoidBeforeSuite.class, "@BeforeSuite method", "must not return a value."),
+			invalidBeforeOrAfterSuiteCase(ParameterAcceptingBeforeSuite.class, "@BeforeSuite method",
+				"must not accept parameters."),
+			invalidBeforeOrAfterSuiteCase(NonStaticBeforeSuite.class, "@BeforeSuite method", "must be static."),
+			invalidBeforeOrAfterSuiteCase(PrivateBeforeSuite.class, "@BeforeSuite method", "must not be private."),
+			invalidBeforeOrAfterSuiteCase(NonVoidAfterSuite.class, "@AfterSuite method", "must not return a value."),
+			invalidBeforeOrAfterSuiteCase(ParameterAcceptingAfterSuite.class, "@AfterSuite method",
+				"must not accept parameters."),
+			invalidBeforeOrAfterSuiteCase(NonStaticAfterSuite.class, "@AfterSuite method", "must be static."),
+			invalidBeforeOrAfterSuiteCase(PrivateAfterSuite.class, "@AfterSuite method", "must not be private."));
+	}
+
+	private static Arguments invalidBeforeOrAfterSuiteCase(Class<?> suiteClass, String failureMessageStart,
+			String failureMessageEnd) {
+		return arguments(named(suiteClass.getSimpleName(), suiteClass),
+			(Predicate<String>) s -> s.startsWith(failureMessageStart) && s.endsWith(failureMessageEnd));
+	}
+
+	private static EngineExecutionResults executeSuite(Class<?> suiteClass) {
+		return EngineTestKit.engine(ENGINE_ID).selectors(selectClass(suiteClass)).execute();
+	}
+
+}
diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/testcases/StatefulTestCase.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/testcases/StatefulTestCase.java
new file mode 100644
index 000000000000..3a2a32230c27
--- /dev/null
+++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/testcases/StatefulTestCase.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2015-2024 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.platform.suite.engine.testcases;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * @since 1.11
+ */
+public class StatefulTestCase {
+
+	public static List<String> callSequence = new ArrayList<>();
+
+	public static class Test1 {
+
+		@Test
+		void statefulTest() {
+			callSequence.add("test1");
+		}
+
+	}
+
+	public static class Test2 {
+
+		@Test
+		void statefulTest() {
+			callSequence.add("test2");
+			fail("This is a failing test");
+		}
+
+	}
+
+}
diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/LifecycleMethodsSuites.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/LifecycleMethodsSuites.java
new file mode 100644
index 000000000000..8a5af3587d38
--- /dev/null
+++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/LifecycleMethodsSuites.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2015-2024 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.platform.suite.engine.testsuites;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.junit.platform.suite.api.AfterSuite;
+import org.junit.platform.suite.api.BeforeSuite;
+import org.junit.platform.suite.api.SelectClasses;
+import org.junit.platform.suite.api.Suite;
+import org.junit.platform.suite.engine.BeforeAndAfterSuiteTests;
+import org.junit.platform.suite.engine.testcases.StatefulTestCase;
+
+/**
+ * Test suites used in {@link BeforeAndAfterSuiteTests}.
+ *
+ * @since 1.11
+ */
+public class LifecycleMethodsSuites {
+
+	@Retention(RetentionPolicy.RUNTIME)
+	@Target(ElementType.TYPE)
+	@Suite
+	@SelectClasses({ StatefulTestCase.Test1.class, StatefulTestCase.Test2.class })
+	private @interface TestSuite {
+	}
+
+	@TestSuite
+	public static class SuccessfulBeforeAndAfterSuite {
+
+		@BeforeSuite
+		static void setUp() {
+			StatefulTestCase.callSequence.add("beforeSuiteMethod");
+		}
+
+		@AfterSuite
+		static void tearDown() {
+			StatefulTestCase.callSequence.add("afterSuiteMethod");
+		}
+
+	}
+
+	@TestSuite
+	public static class FailingBeforeSuite {
+
+		@BeforeSuite
+		static void setUp() {
+			StatefulTestCase.callSequence.add("beforeSuiteMethod");
+			throw new RuntimeException("Exception thrown by @BeforeSuite method");
+		}
+
+		@AfterSuite
+		static void tearDown() {
+			StatefulTestCase.callSequence.add("afterSuiteMethod");
+		}
+
+	}
+
+	@TestSuite
+	public static class FailingAfterSuite {
+
+		@BeforeSuite
+		static void setUp() {
+			StatefulTestCase.callSequence.add("beforeSuiteMethod");
+		}
+
+		@AfterSuite
+		static void tearDown() {
+			StatefulTestCase.callSequence.add("afterSuiteMethod");
+			throw new RuntimeException("Exception thrown by @AfterSuite method");
+		}
+
+	}
+
+	@TestSuite
+	public static class FailingBeforeAndAfterSuite {
+
+		@BeforeSuite
+		static void setUp() {
+			StatefulTestCase.callSequence.add("beforeSuiteMethod");
+			throw new RuntimeException("Exception thrown by @BeforeSuite method");
+		}
+
+		@AfterSuite
+		static void tearDown() {
+			StatefulTestCase.callSequence.add("afterSuiteMethod");
+			throw new RuntimeException("Exception thrown by @AfterSuite method");
+		}
+
+	}
+
+	@TestSuite
+	public static class SeveralFailingBeforeAndAfterSuite {
+
+		@BeforeSuite
+		static void setUp1() {
+			StatefulTestCase.callSequence.add("beforeSuiteMethod");
+			throw new RuntimeException("Exception thrown by @BeforeSuite method");
+		}
+
+		@BeforeSuite
+		static void setUp2() {
+			StatefulTestCase.callSequence.add("beforeSuiteMethod");
+			throw new RuntimeException("Exception thrown by @BeforeSuite method");
+		}
+
+		@AfterSuite
+		static void tearDown1() {
+			StatefulTestCase.callSequence.add("afterSuiteMethod");
+			throw new RuntimeException("Exception thrown by @AfterSuite method");
+		}
+
+		@AfterSuite
+		static void tearDown2() {
+			StatefulTestCase.callSequence.add("afterSuiteMethod");
+			throw new RuntimeException("Exception thrown by @AfterSuite method");
+		}
+
+	}
+
+	@TestSuite
+	public static class SuperclassWithBeforeAndAfterSuite {
+
+		@BeforeSuite
+		static void setUp() {
+			StatefulTestCase.callSequence.add("superclassBeforeSuiteMethod");
+		}
+
+		@AfterSuite
+		static void tearDown() {
+			StatefulTestCase.callSequence.add("superclassAfterSuiteMethod");
+		}
+
+	}
+
+	public static class SubclassWithBeforeAndAfterSuite extends SuperclassWithBeforeAndAfterSuite {
+
+		@BeforeSuite
+		static void setUp() {
+			StatefulTestCase.callSequence.add("subclassBeforeSuiteMethod");
+		}
+
+		@AfterSuite
+		static void tearDown() {
+			StatefulTestCase.callSequence.add("subclassAfterSuiteMethod");
+		}
+
+	}
+
+	@TestSuite
+	public static class NonVoidBeforeSuite {
+
+		@BeforeSuite
+		static String nonVoidBeforeSuite() {
+			fail("Should not be called");
+			return "";
+		}
+
+	}
+
+	@TestSuite
+	public static class ParameterAcceptingBeforeSuite {
+
+		@BeforeSuite
+		static void parameterAcceptingBeforeSuite(String param) {
+			fail("Should not be called");
+		}
+
+	}
+
+	@TestSuite
+	public static class NonStaticBeforeSuite {
+
+		@BeforeSuite
+		void nonStaticBeforeSuite() {
+			fail("Should not be called");
+		}
+
+	}
+
+	@TestSuite
+	public static class PrivateBeforeSuite {
+
+		@BeforeSuite
+		private static void privateBeforeSuite() {
+			fail("Should not be called");
+		}
+
+	}
+
+	@TestSuite
+	public static class NonVoidAfterSuite {
+
+		@AfterSuite
+		static String nonVoidAfterSuite() {
+			fail("Should not be called");
+			return "";
+		}
+
+	}
+
+	@TestSuite
+	public static class ParameterAcceptingAfterSuite {
+
+		@AfterSuite
+		static void parameterAcceptingAfterSuite(String param) {
+			fail("Should not be called");
+		}
+
+	}
+
+	@TestSuite
+	public static class NonStaticAfterSuite {
+
+		@AfterSuite
+		void nonStaticAfterSuite() {
+			fail("Should not be called");
+		}
+
+	}
+
+	@TestSuite
+	public static class PrivateAfterSuite {
+
+		@AfterSuite
+		private static void privateAfterSuite() {
+			fail("Should not be called");
+		}
+
+	}
+
+}