diff --git a/.gitlab/pipeline/test.yml b/.gitlab/pipeline/test.yml
index 00a0aa2c9771c09c29d98840473154ea8427bf7c..6fb35c61e48d13cdbe4f9f1645fd2edb6feced94 100644
--- a/.gitlab/pipeline/test.yml
+++ b/.gitlab/pipeline/test.yml
@@ -110,6 +110,30 @@ test-linux-stable-codecov:
         codecovcli -v do-upload -f target/coverage/result/report-${CI_NODE_INDEX}.lcov --disable-search -t ${CODECOV_TOKEN} -r paritytech/polkadot-sdk --commit-sha ${CI_COMMIT_SHA} --fail-on-error --git-service github;
       fi
 
+# some tests do not run with `try-runtime` feature enabled
+# https://github.com/paritytech/polkadot-sdk/pull/4251#discussion_r1624282143
+test-linux-stable-no-try-runtime:
+  stage: test
+  extends:
+    - .docker-env
+    - .common-refs
+    - .run-immediately
+    - .pipeline-stopper-artifacts
+  variables:
+    RUST_TOOLCHAIN: stable
+    # Enable debug assertions since we are running optimized builds for testing
+    # but still want to have debug assertions.
+    RUSTFLAGS: "-Cdebug-assertions=y -Dwarnings"
+  script:
+    - >
+      time cargo nextest run \
+        --workspace \
+        --locked \
+        --release \
+        --no-fail-fast \
+        --cargo-quiet \
+        --features experimental,riscv,ci-only-tests
+
 test-doc:
   stage: test
   extends:
diff --git a/Cargo.lock b/Cargo.lock
index 69eabeb04e20c6c5c116f800e73a411eee370f5a..61f485bcecb3f5c713bdb1de143bb1e5108d144c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11840,6 +11840,7 @@ dependencies = [
 name = "pallet-migrations"
 version = "1.0.0"
 dependencies = [
+ "cfg-if",
  "docify",
  "frame-benchmarking",
  "frame-executive",
diff --git a/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs b/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs
index 150152964b90e3aa45011dc8e83ff3c38556566a..16e023ad3dc48577f8ebd4018e174c88a099393e 100644
--- a/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs
+++ b/cumulus/parachains/runtimes/people/people-rococo/src/lib.rs
@@ -111,6 +111,7 @@ pub type UncheckedExtrinsic =
 /// Migrations to apply on runtime upgrade.
 pub type Migrations = (
 	pallet_collator_selection::migration::v2::MigrationToV2<Runtime>,
+	cumulus_pallet_xcmp_queue::migration::v5::MigrateV4ToV5<Runtime>,
 	// permanent
 	pallet_xcm::migration::MigrateToLatestXcmVersion<Runtime>,
 );
diff --git a/prdoc/pr_4251.prdoc b/prdoc/pr_4251.prdoc
new file mode 100644
index 0000000000000000000000000000000000000000..4d4fcd73469232939c8f25e8861847d5f61bbb1b
--- /dev/null
+++ b/prdoc/pr_4251.prdoc
@@ -0,0 +1,79 @@
+title: MBM `try-runtime` support
+doc:
+- audience: Runtime Dev
+  description: |
+    # MBM try-runtime support
+
+    This MR adds support to the try-runtime
+    trait such that the try-runtime-CLI will be able to support MBM testing [here](https://github.com/paritytech/try-runtime-cli/pull/90).
+    It mainly adds two feature-gated hooks to the `SteppedMigration` hook to facilitate
+    testing. These hooks are named `pre_upgrade` and `post_upgrade` and have the
+    same signature and implications as for single-block migrations.
+
+    ## Integration
+        
+    To make use of this in your Multi-Block-Migration, just implement the two new hooks and test pre- and post-conditions in them:
+
+    ```rust
+    #[cfg(feature = "try-runtime")]
+    fn pre_upgrade() -> Result<Vec<u8>, frame_support::sp_runtime::TryRuntimeError>
+    {
+      // ...
+    }
+
+    #[cfg(feature = "try-runtime")]
+    fn post_upgrade(prev: Vec<u8>) -> Result<(), frame_support::sp_runtime::TryRuntimeError> {
+        // ...
+    }
+    ```
+
+    You may return an error or panic in these functions to indicate failure.
+    This will then show up in the try-runtime-CLI and can be used in CI for testing.
+
+        
+    Changes:
+    - Adds `try-runtime` gated methods `pre_upgrade` and `post_upgrade`
+      on `SteppedMigration`
+    - Adds `try-runtime` gated methods `nth_pre_upgrade`
+      and `nth_post_upgrade` on `SteppedMigrations`
+    - Modifies `pallet_migrations`
+      implementation to run pre_upgrade and post_upgrade steps at the appropriate times, and panic in the event of migration failure.
+crates:
+- name: asset-hub-rococo-runtime
+  bump: minor
+- name: asset-hub-westend-runtime
+  bump: minor
+- name: bridge-hub-rococo-runtime
+  bump: minor
+- name: bridge-hub-westend-runtime
+  bump: minor
+- name: collectives-westend-runtime
+  bump: minor
+- name: contracts-rococo-runtime
+  bump: minor
+- name: coretime-rococo-runtime
+  bump: minor
+- name: coretime-westend-runtime
+  bump: minor
+- name: people-rococo-runtime
+  bump: minor
+- name: people-westend-runtime
+  bump: minor
+- name: penpal-runtime
+  bump: minor
+- name: polkadot-parachain-bin
+  bump: minor
+- name: rococo-runtime
+  bump: minor
+- name: westend-runtime
+  bump: minor
+- name: frame-executive
+  bump: minor
+- name: pallet-migrations
+  bump: minor
+- name: frame-support
+  bump: minor
+- name: frame-system
+  bump: minor
+- name: frame-try-runtime
+  bump: minor
diff --git a/substrate/frame/examples/multi-block-migrations/src/migrations/v1/mod.rs b/substrate/frame/examples/multi-block-migrations/src/migrations/v1/mod.rs
index 2016b03de45e53e94a77e29a87f4cb70f8e17e9b..6243846d86b06f7938541865756d3511bbbb9ae5 100644
--- a/substrate/frame/examples/multi-block-migrations/src/migrations/v1/mod.rs
+++ b/substrate/frame/examples/multi-block-migrations/src/migrations/v1/mod.rs
@@ -21,6 +21,8 @@
 //! [`v0::MyMap`](`crate::migrations::v1::v0::MyMap`) storage map, transforms them,
 //! and inserts them into the [`MyMap`](`crate::pallet::MyMap`) storage map.
 
+extern crate alloc;
+
 use super::PALLET_MIGRATIONS_ID;
 use crate::pallet::{Config, MyMap};
 use frame_support::{
@@ -29,6 +31,12 @@ use frame_support::{
 	weights::WeightMeter,
 };
 
+#[cfg(feature = "try-runtime")]
+use alloc::collections::btree_map::BTreeMap;
+
+#[cfg(feature = "try-runtime")]
+use alloc::vec::Vec;
+
 mod benchmarks;
 mod tests;
 pub mod weights;
@@ -115,4 +123,39 @@ impl<T: Config, W: weights::WeightInfo> SteppedMigration for LazyMigrationV1<T,
 		}
 		Ok(cursor)
 	}
+
+	#[cfg(feature = "try-runtime")]
+	fn pre_upgrade() -> Result<Vec<u8>, frame_support::sp_runtime::TryRuntimeError> {
+		use codec::Encode;
+
+		// Return the state of the storage before the migration.
+		Ok(v0::MyMap::<T>::iter().collect::<BTreeMap<_, _>>().encode())
+	}
+
+	#[cfg(feature = "try-runtime")]
+	fn post_upgrade(prev: Vec<u8>) -> Result<(), frame_support::sp_runtime::TryRuntimeError> {
+		use codec::Decode;
+
+		// Check the state of the storage after the migration.
+		let prev_map = BTreeMap::<u32, u32>::decode(&mut &prev[..])
+			.expect("Failed to decode the previous storage state");
+
+		// Check the len of prev and post are the same.
+		assert_eq!(
+			MyMap::<T>::iter().count(),
+			prev_map.len(),
+			"Migration failed: the number of items in the storage after the migration is not the same as before"
+		);
+
+		for (key, value) in prev_map {
+			let new_value =
+				MyMap::<T>::get(key).expect("Failed to get the value after the migration");
+			assert_eq!(
+				value as u64, new_value,
+				"Migration failed: the value after the migration is not the same as before"
+			);
+		}
+
+		Ok(())
+	}
 }
diff --git a/substrate/frame/migrations/Cargo.toml b/substrate/frame/migrations/Cargo.toml
index 5fbed74a4400704db6610ec54e3aba8dd234cdae..a32e48e652805f91fabf340916351f6c27d9b79a 100644
--- a/substrate/frame/migrations/Cargo.toml
+++ b/substrate/frame/migrations/Cargo.toml
@@ -12,6 +12,7 @@ targets = ["x86_64-unknown-linux-gnu"]
 
 [dependencies]
 codec = { features = ["derive"], workspace = true }
+cfg-if = { workspace = true }
 docify = { workspace = true }
 impl-trait-for-tuples = { workspace = true }
 log = { workspace = true, default-features = true }
diff --git a/substrate/frame/migrations/src/lib.rs b/substrate/frame/migrations/src/lib.rs
index 1823e5a2f9527fb7e02c06132cf42a7083fc6e1d..d9490e7dcfe99fed3cc70560f6f25e61f4fc190f 100644
--- a/substrate/frame/migrations/src/lib.rs
+++ b/substrate/frame/migrations/src/lib.rs
@@ -70,21 +70,26 @@
 //! points to the currently active migration and stores its inner cursor. The inner cursor can then
 //! be used by the migration to store its inner state and advance. Each time when the migration
 //! returns `Some(cursor)`, it signals the pallet that it is not done yet.
+//!
 //! The cursor is reset on each runtime upgrade. This ensures that it starts to execute at the
 //! first migration in the vector. The pallets cursor is only ever incremented or set to `Stuck`
 //! once it encounters an error (Goal 4). Once in the stuck state, the pallet will stay stuck until
 //! it is fixed through manual governance intervention.
+//!
 //! As soon as the cursor of the pallet becomes `Some(_)`; [`MultiStepMigrator::ongoing`] returns
 //! `true` (Goal 2). This can be used by upstream code to possibly pause transactions.
 //! In `on_initialize` the pallet will load the current migration and check whether it was already
 //! executed in the past by checking for membership of its ID in the [`Historic`] set. Historic
 //! migrations are skipped without causing an error. Each successfully executed migration is added
 //! to this set (Goal 5).
+//!
 //! This proceeds until no more migrations remain. At that point, the event `UpgradeCompleted` is
 //! emitted (Goal 1).
+//!
 //! The execution of each migration happens by calling [`SteppedMigration::transactional_step`].
 //! This function wraps the inner `step` function into a transactional layer to allow rollback in
 //! the error case (Goal 6).
+//!
 //! Weight limits must be checked by the migration itself. The pallet provides a [`WeightMeter`] for
 //! that purpose. The pallet may return [`SteppedMigrationError::InsufficientWeight`] at any point.
 //! In that scenario, one of two things will happen: if that migration was exclusively executed
@@ -156,11 +161,15 @@ use core::ops::ControlFlow;
 use frame_support::{
 	defensive, defensive_assert,
 	migrations::*,
+	pallet_prelude::*,
 	traits::Get,
 	weights::{Weight, WeightMeter},
 	BoundedVec,
 };
-use frame_system::{pallet_prelude::BlockNumberFor, Pallet as System};
+use frame_system::{
+	pallet_prelude::{BlockNumberFor, *},
+	Pallet as System,
+};
 use sp_runtime::Saturating;
 
 /// Points to the next migration to execute.
@@ -262,6 +271,7 @@ pub type IdentifierOf<T> = BoundedVec<u8, <T as Config>::IdentifierMaxLen>;
 pub type ActiveCursorOf<T> = ActiveCursor<RawCursorOf<T>, BlockNumberFor<T>>;
 
 /// Trait for a tuple of No-OP migrations with one element.
+#[impl_trait_for_tuples::impl_for_tuples(30)]
 pub trait MockedMigrations: SteppedMigrations {
 	/// The migration should fail after `n` steps.
 	fn set_fail_after(n: u32);
@@ -269,11 +279,24 @@ pub trait MockedMigrations: SteppedMigrations {
 	fn set_success_after(n: u32);
 }
 
+#[cfg(feature = "try-runtime")]
+/// Wrapper for pre-upgrade bytes, allowing us to impl MEL on it.
+///
+/// For `try-runtime` testing only.
+#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode, scale_info::TypeInfo, Default)]
+struct PreUpgradeBytesWrapper(pub Vec<u8>);
+
+/// Data stored by the pre-upgrade hook of the MBMs. Only used for `try-runtime` testing.
+///
+/// Define this outside of the pallet so it is not confused with actual storage.
+#[cfg(feature = "try-runtime")]
+#[frame_support::storage_alias]
+type PreUpgradeBytes<T: Config> =
+	StorageMap<Pallet<T>, Twox64Concat, IdentifierOf<T>, PreUpgradeBytesWrapper, ValueQuery>;
+
 #[frame_support::pallet]
 pub mod pallet {
 	use super::*;
-	use frame_support::pallet_prelude::*;
-	use frame_system::pallet_prelude::*;
 
 	#[pallet::pallet]
 	pub struct Pallet<T>(_);
@@ -701,6 +724,16 @@ impl<T: Config> Pallet<T> {
 		}
 
 		let max_steps = T::Migrations::nth_max_steps(cursor.index);
+
+		// If this is the first time running this migration, exec the pre-upgrade hook.
+		#[cfg(feature = "try-runtime")]
+		if !PreUpgradeBytes::<T>::contains_key(&bounded_id) {
+			let bytes = T::Migrations::nth_pre_upgrade(cursor.index)
+				.expect("Invalid cursor.index")
+				.expect("Pre-upgrade failed");
+			PreUpgradeBytes::<T>::insert(&bounded_id, PreUpgradeBytesWrapper(bytes));
+		}
+
 		let next_cursor = T::Migrations::nth_transactional_step(
 			cursor.index,
 			cursor.inner_cursor.clone().map(|c| c.into_inner()),
@@ -735,6 +768,16 @@ impl<T: Config> Pallet<T> {
 			},
 			Ok(None) => {
 				// A migration is done when it returns cursor `None`.
+
+				// Run post-upgrade checks.
+				#[cfg(feature = "try-runtime")]
+				T::Migrations::nth_post_upgrade(
+					cursor.index,
+					PreUpgradeBytes::<T>::get(&bounded_id).0,
+				)
+				.expect("Invalid cursor.index.")
+				.expect("Post-upgrade failed.");
+
 				Self::deposit_event(Event::MigrationCompleted { index: cursor.index, took });
 				Historic::<T>::insert(&bounded_id, ());
 				cursor.goto_next_migration(System::<T>::block_number());
@@ -759,14 +802,21 @@ impl<T: Config> Pallet<T> {
 	}
 
 	/// Fail the current runtime upgrade, caused by `migration`.
+	///
+	/// When the `try-runtime` feature is enabled, this function will panic.
+	// Allow unreachable code so it can compile without warnings when `try-runtime` is enabled.
 	fn upgrade_failed(migration: Option<u32>) {
 		use FailedMigrationHandling::*;
 		Self::deposit_event(Event::UpgradeFailed);
 
-		match T::FailedMigrationHandler::failed(migration) {
-			KeepStuck => Cursor::<T>::set(Some(MigrationCursor::Stuck)),
-			ForceUnstuck => Cursor::<T>::kill(),
-			Ignore => {},
+		if cfg!(feature = "try-runtime") {
+			panic!("Migration with index {:?} failed.", migration);
+		} else {
+			match T::FailedMigrationHandler::failed(migration) {
+				KeepStuck => Cursor::<T>::set(Some(MigrationCursor::Stuck)),
+				ForceUnstuck => Cursor::<T>::kill(),
+				Ignore => {},
+			}
 		}
 	}
 
diff --git a/substrate/frame/migrations/src/mock_helpers.rs b/substrate/frame/migrations/src/mock_helpers.rs
index 9d3b4d1193f21e8a0fd35463d161cc4df978146a..a03c70051d308d7882e8dca68bc27fcd4636f510 100644
--- a/substrate/frame/migrations/src/mock_helpers.rs
+++ b/substrate/frame/migrations/src/mock_helpers.rs
@@ -43,6 +43,12 @@ pub enum MockedMigrationKind {
 	/// Cause an [`SteppedMigrationError::InsufficientWeight`] error after its number of steps
 	/// elapsed.
 	HighWeightAfter(Weight),
+	/// PreUpgrade should fail.
+	#[cfg(feature = "try-runtime")]
+	PreUpgradeFail,
+	/// PostUpgrade should fail.
+	#[cfg(feature = "try-runtime")]
+	PostUpgradeFail,
 }
 use MockedMigrationKind::*; // C style
 
@@ -99,6 +105,8 @@ impl SteppedMigrations for MockedMigrations {
 				Err(SteppedMigrationError::Failed)
 			},
 			TimeoutAfter => unreachable!(),
+			#[cfg(feature = "try-runtime")]
+			PreUpgradeFail | PostUpgradeFail => Ok(None),
 		})
 	}
 
@@ -115,6 +123,31 @@ impl SteppedMigrations for MockedMigrations {
 		MIGRATIONS::get().get(n as usize).map(|(_, s)| Some(*s))
 	}
 
+	#[cfg(feature = "try-runtime")]
+	fn nth_pre_upgrade(n: u32) -> Option<Result<Vec<u8>, sp_runtime::TryRuntimeError>> {
+		let (kind, _) = MIGRATIONS::get()[n as usize];
+
+		if let PreUpgradeFail = kind {
+			return Some(Err("Some pre-upgrade error".into()))
+		}
+
+		Some(Ok(vec![]))
+	}
+
+	#[cfg(feature = "try-runtime")]
+	fn nth_post_upgrade(
+		n: u32,
+		_state: Vec<u8>,
+	) -> Option<Result<(), sp_runtime::TryRuntimeError>> {
+		let (kind, _) = MIGRATIONS::get()[n as usize];
+
+		if let PostUpgradeFail = kind {
+			return Some(Err("Some post-upgrade error".into()))
+		}
+
+		Some(Ok(()))
+	}
+
 	fn cursor_max_encoded_len() -> usize {
 		65_536
 	}
diff --git a/substrate/frame/migrations/src/tests.rs b/substrate/frame/migrations/src/tests.rs
index 73ca2a9a09cf6451c65b0927d6090ef7452e8554..55f212bcf373cfb921a96f3b44a95d2f2b7f1611 100644
--- a/substrate/frame/migrations/src/tests.rs
+++ b/substrate/frame/migrations/src/tests.rs
@@ -17,12 +17,13 @@
 
 #![cfg(test)]
 
+use frame_support::{pallet_prelude::Weight, traits::OnRuntimeUpgrade};
+
 use crate::{
 	mock::{Test as T, *},
 	mock_helpers::{MockedMigrationKind::*, *},
 	Cursor, Event, FailedMigrationHandling, MigrationCursor,
 };
-use frame_support::{pallet_prelude::Weight, traits::OnRuntimeUpgrade};
 
 #[docify::export]
 #[test]
@@ -86,6 +87,7 @@ fn simple_multiple_works() {
 }
 
 #[test]
+#[cfg_attr(feature = "try-runtime", should_panic)]
 fn failing_migration_sets_cursor_to_stuck() {
 	test_closure(|| {
 		FailedUpgradeResponse::set(FailedMigrationHandling::KeepStuck);
@@ -116,6 +118,7 @@ fn failing_migration_sets_cursor_to_stuck() {
 }
 
 #[test]
+#[cfg_attr(feature = "try-runtime", should_panic)]
 fn failing_migration_force_unstuck_works() {
 	test_closure(|| {
 		FailedUpgradeResponse::set(FailedMigrationHandling::ForceUnstuck);
@@ -148,6 +151,7 @@ fn failing_migration_force_unstuck_works() {
 /// A migration that reports not getting enough weight errors if it is the first one to run in that
 /// block.
 #[test]
+#[cfg_attr(feature = "try-runtime", should_panic)]
 fn high_weight_migration_singular_fails() {
 	test_closure(|| {
 		MockedMigrations::set(vec![(HighWeightAfter(Weight::zero()), 2)]);
@@ -176,6 +180,7 @@ fn high_weight_migration_singular_fails() {
 /// A migration that reports of not getting enough weight is retried once, if it is not the first
 /// one to run in a block.
 #[test]
+#[cfg_attr(feature = "try-runtime", should_panic)]
 fn high_weight_migration_retries_once() {
 	test_closure(|| {
 		MockedMigrations::set(vec![(SucceedAfter, 0), (HighWeightAfter(Weight::zero()), 0)]);
@@ -205,6 +210,7 @@ fn high_weight_migration_retries_once() {
 // Note: Same as `high_weight_migration_retries_once` but with different required weight for the
 // migration.
 #[test]
+#[cfg_attr(feature = "try-runtime", should_panic)]
 fn high_weight_migration_permanently_overweight_fails() {
 	test_closure(|| {
 		MockedMigrations::set(vec![(SucceedAfter, 0), (HighWeightAfter(Weight::MAX), 0)]);
@@ -300,6 +306,7 @@ fn historic_skipping_works() {
 /// When another upgrade happens while a migration is still running, it should set the cursor to
 /// stuck.
 #[test]
+#[cfg_attr(feature = "try-runtime", should_panic)]
 fn upgrade_fails_when_migration_active() {
 	test_closure(|| {
 		MockedMigrations::set(vec![(SucceedAfter, 10)]);
@@ -326,6 +333,7 @@ fn upgrade_fails_when_migration_active() {
 }
 
 #[test]
+#[cfg_attr(feature = "try-runtime", should_panic)]
 fn migration_timeout_errors() {
 	test_closure(|| {
 		MockedMigrations::set(vec![(TimeoutAfter, 3)]);
@@ -358,3 +366,91 @@ fn migration_timeout_errors() {
 		assert_eq!(upgrades_started_completed_failed(), (0, 0, 1));
 	});
 }
+
+#[cfg(feature = "try-runtime")]
+#[test]
+fn try_runtime_success_case() {
+	use Event::*;
+	test_closure(|| {
+		// Add three migrations, each taking one block longer than the previous.
+		MockedMigrations::set(vec![(SucceedAfter, 0), (SucceedAfter, 1), (SucceedAfter, 2)]);
+
+		System::set_block_number(1);
+		Migrations::on_runtime_upgrade();
+		run_to_block(10);
+
+		// Check that we got all events.
+		assert_events(vec![
+			UpgradeStarted { migrations: 3 },
+			MigrationCompleted { index: 0, took: 1 },
+			MigrationAdvanced { index: 1, took: 0 },
+			MigrationCompleted { index: 1, took: 1 },
+			MigrationAdvanced { index: 2, took: 0 },
+			MigrationAdvanced { index: 2, took: 1 },
+			MigrationCompleted { index: 2, took: 2 },
+			UpgradeCompleted,
+		]);
+	});
+}
+
+#[test]
+#[cfg(feature = "try-runtime")]
+#[should_panic]
+fn try_runtime_pre_upgrade_failure() {
+	test_closure(|| {
+		// Add three migrations, it should fail after the second one.
+		MockedMigrations::set(vec![(SucceedAfter, 0), (PreUpgradeFail, 1), (SucceedAfter, 2)]);
+
+		System::set_block_number(1);
+		Migrations::on_runtime_upgrade();
+
+		// should panic
+		run_to_block(10);
+	});
+}
+
+#[test]
+#[cfg(feature = "try-runtime")]
+#[should_panic]
+fn try_runtime_post_upgrade_failure() {
+	test_closure(|| {
+		// Add three migrations, it should fail after the second one.
+		MockedMigrations::set(vec![(SucceedAfter, 0), (PostUpgradeFail, 1), (SucceedAfter, 2)]);
+
+		System::set_block_number(1);
+		Migrations::on_runtime_upgrade();
+
+		// should panic
+		run_to_block(10);
+	});
+}
+
+#[test]
+#[cfg(feature = "try-runtime")]
+#[should_panic]
+fn try_runtime_migration_failure() {
+	test_closure(|| {
+		// Add three migrations, it should fail after the second one.
+		MockedMigrations::set(vec![(SucceedAfter, 0), (FailAfter, 5), (SucceedAfter, 10)]);
+
+		System::set_block_number(1);
+		Migrations::on_runtime_upgrade();
+
+		// should panic
+		run_to_block(10);
+	});
+}
+
+#[test]
+fn try_runtime_no_migrations() {
+	test_closure(|| {
+		MockedMigrations::set(vec![]);
+
+		System::set_block_number(1);
+		Migrations::on_runtime_upgrade();
+
+		run_to_block(10);
+
+		assert_eq!(System::events().len(), 0);
+	});
+}
diff --git a/substrate/frame/support/src/migrations.rs b/substrate/frame/support/src/migrations.rs
index 0eabf9d0ee162a5f4bc887119a6c9923aa8e7f1a..905d6143e4f1c9ba97c73963fd97b165e51adb06 100644
--- a/substrate/frame/support/src/migrations.rs
+++ b/substrate/frame/support/src/migrations.rs
@@ -529,6 +529,25 @@ pub trait SteppedMigration {
 		})
 		.map_err(|()| SteppedMigrationError::Failed)?
 	}
+
+	/// Hook for testing that is run before the migration is started.
+	///
+	/// Returns some bytes which are passed into `post_upgrade` after the migration is completed.
+	/// This is not run for the real migration, so panicking is not an issue here.
+	#[cfg(feature = "try-runtime")]
+	fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
+		Ok(Vec::new())
+	}
+
+	/// Hook for testing that is run after the migration is completed.
+	///
+	/// Should be used to verify the state of the chain after the migration. The `state` parameter
+	/// is the return value from `pre_upgrade`. This is not run for the real migration, so panicking
+	/// is not an issue here.
+	#[cfg(feature = "try-runtime")]
+	fn post_upgrade(_state: Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
+		Ok(())
+	}
 }
 
 /// Error that can occur during a [`SteppedMigration`].
@@ -700,6 +719,19 @@ pub trait SteppedMigrations {
 		meter: &mut WeightMeter,
 	) -> Option<Result<Option<Vec<u8>>, SteppedMigrationError>>;
 
+	/// Call the pre-upgrade hooks of the `n`th migration.
+	///
+	/// Returns `None` if the index is out of bounds.
+	#[cfg(feature = "try-runtime")]
+	fn nth_pre_upgrade(n: u32) -> Option<Result<Vec<u8>, sp_runtime::TryRuntimeError>>;
+
+	/// Call the post-upgrade hooks of the `n`th migration.
+	///
+	/// Returns `None` if the index is out of bounds.
+	#[cfg(feature = "try-runtime")]
+	fn nth_post_upgrade(n: u32, _state: Vec<u8>)
+		-> Option<Result<(), sp_runtime::TryRuntimeError>>;
+
 	/// The maximal encoded length across all cursors.
 	fn cursor_max_encoded_len() -> usize;
 
@@ -763,6 +795,19 @@ impl SteppedMigrations for () {
 		None
 	}
 
+	#[cfg(feature = "try-runtime")]
+	fn nth_pre_upgrade(_n: u32) -> Option<Result<Vec<u8>, sp_runtime::TryRuntimeError>> {
+		Some(Ok(Vec::new()))
+	}
+
+	#[cfg(feature = "try-runtime")]
+	fn nth_post_upgrade(
+		_n: u32,
+		_state: Vec<u8>,
+	) -> Option<Result<(), sp_runtime::TryRuntimeError>> {
+		Some(Ok(()))
+	}
+
 	fn cursor_max_encoded_len() -> usize {
 		0
 	}
@@ -792,11 +837,11 @@ impl<T: SteppedMigration> SteppedMigrations for T {
 	}
 
 	fn nth_step(
-		_n: u32,
+		n: u32,
 		cursor: Option<Vec<u8>>,
 		meter: &mut WeightMeter,
 	) -> Option<Result<Option<Vec<u8>>, SteppedMigrationError>> {
-		if !_n.is_zero() {
+		if !n.is_zero() {
 			defensive!("nth_step should only be called with n==0");
 			return None
 		}
@@ -835,6 +880,23 @@ impl<T: SteppedMigration> SteppedMigrations for T {
 		)
 	}
 
+	#[cfg(feature = "try-runtime")]
+	fn nth_pre_upgrade(n: u32) -> Option<Result<Vec<u8>, sp_runtime::TryRuntimeError>> {
+		if n != 0 {
+			defensive!("nth_pre_upgrade should only be called with n==0");
+		}
+
+		Some(T::pre_upgrade())
+	}
+
+	#[cfg(feature = "try-runtime")]
+	fn nth_post_upgrade(n: u32, state: Vec<u8>) -> Option<Result<(), sp_runtime::TryRuntimeError>> {
+		if n != 0 {
+			defensive!("nth_post_upgrade should only be called with n==0");
+		}
+		Some(T::post_upgrade(state))
+	}
+
 	fn cursor_max_encoded_len() -> usize {
 		T::Cursor::max_encoded_len()
 	}
@@ -900,6 +962,36 @@ impl SteppedMigrations for Tuple {
 		None
 	}
 
+	#[cfg(feature = "try-runtime")]
+	fn nth_pre_upgrade(n: u32) -> Option<Result<Vec<u8>, sp_runtime::TryRuntimeError>> {
+		let mut i = 0;
+
+		for_tuples! ( #(
+			if (i + Tuple::len()) > n {
+				return Tuple::nth_pre_upgrade(n - i)
+			}
+
+			i += Tuple::len();
+		)* );
+
+		None
+	}
+
+	#[cfg(feature = "try-runtime")]
+	fn nth_post_upgrade(n: u32, state: Vec<u8>) -> Option<Result<(), sp_runtime::TryRuntimeError>> {
+		let mut i = 0;
+
+		for_tuples! ( #(
+			if (i + Tuple::len()) > n {
+				return Tuple::nth_post_upgrade(n - i, state)
+			}
+
+			i += Tuple::len();
+		)* );
+
+		None
+	}
+
 	fn nth_max_steps(n: u32) -> Option<Option<u32>> {
 		let mut i = 0;
 
diff --git a/substrate/frame/support/src/traits/try_runtime/mod.rs b/substrate/frame/support/src/traits/try_runtime/mod.rs
index 09c33c01440677e14eacbd92063f7ffafb267c71..284ba3d7422da26122da07a135f20abee18b8382 100644
--- a/substrate/frame/support/src/traits/try_runtime/mod.rs
+++ b/substrate/frame/support/src/traits/try_runtime/mod.rs
@@ -28,7 +28,7 @@ use sp_arithmetic::traits::AtLeast32BitUnsigned;
 use sp_runtime::TryRuntimeError;
 
 /// Which state tests to execute.
-#[derive(codec::Encode, codec::Decode, Clone, scale_info::TypeInfo)]
+#[derive(codec::Encode, codec::Decode, Clone, scale_info::TypeInfo, PartialEq)]
 pub enum Select {
 	/// None of them.
 	None,
@@ -95,7 +95,7 @@ impl std::str::FromStr for Select {
 }
 
 /// Select which checks should be run when trying a runtime upgrade upgrade.
-#[derive(codec::Encode, codec::Decode, Clone, Debug, Copy, scale_info::TypeInfo)]
+#[derive(codec::Encode, codec::Decode, Clone, Debug, Copy, scale_info::TypeInfo, PartialEq)]
 pub enum UpgradeCheckSelect {
 	/// Run no checks.
 	None,