diff --git a/pages/interop/tutorials/unit-tests.mdx b/pages/interop/tutorials/unit-tests.mdx
new file mode 100644
index 000000000..e5467827f
--- /dev/null
+++ b/pages/interop/tutorials/unit-tests.mdx
@@ -0,0 +1,29 @@
+---
+title: Interop and unit tests
+description: Learn how to write unit tests for interop
+lang: en-US
+content_type: landing-page
+topic: interop-tutorial-unit-tests
+personas:
+ - app-developer
+categories:
+ - protocol
+ - interoperability
+ - cross-chain-messaging
+ - tutorial
+ - testing
+is_imported_content: 'false'
+---
+
+import { Card, Cards } from 'nextra/components'
+
+# Interop and unit tests
+
+Documentation covering Interop related tutorials.
+
+
+ } />
+
+ } />
+
+
diff --git a/pages/interop/tutorials/unit-tests/_meta.json b/pages/interop/tutorials/unit-tests/_meta.json
new file mode 100644
index 000000000..e9cbbaa02
--- /dev/null
+++ b/pages/interop/tutorials/unit-tests/_meta.json
@@ -0,0 +1,4 @@
+{
+ "foundry": "Foundry",
+ "hardhat": "Hardhat"
+}
diff --git a/pages/interop/tutorials/unit-tests/foundry.mdx b/pages/interop/tutorials/unit-tests/foundry.mdx
new file mode 100644
index 000000000..bf5f7156f
--- /dev/null
+++ b/pages/interop/tutorials/unit-tests/foundry.mdx
@@ -0,0 +1,147 @@
+---
+title: Interop and unit tests using Foundry
+description: Learn how to write unit tests for interop when using Foundry
+lang: en-US
+content_type: tutorial
+topic: interop-tutorial-unit-tests-foundry
+personas:
+ - app-developer
+categories:
+ - protocol
+ - interoperability
+ - cross-chain-messaging
+ - tutorial
+ - testing
+ - foundry
+is_imported_content: 'false'
+---
+
+import { Callout, Steps } from 'nextra/components'
+import { InteropCallout } from '@/components/WipCallout'
+
+
+
+# Interop and unit tests using Foundry
+
+Most parts of the contracts can be [tested normally](/app-developers/testing-apps).
+This tutorial teaches you how to verify that a message has been sent successfully and to simulate receiving a message, the two functions that tie directly into interop.
+To show how this works, we test [the `Greeter` and `GreetingSender` contracts](/interop/tutorials/message-passing).
+
+## Setup
+
+This script creates a Foundry project (in `testing/forge`) and a Hardhat project (in `testing/hardhat`) with the necessary files.
+Execute it in a UNIX or Linux environment.
+
+The results of this step are similar to what the [message passing tutorial](/interop/tutorials/message-passing) produces.
+
+```sh file=/public/tutorials/setup-for-testing.sh hash=4d0fa175564131911ab6a12fb110294c
+```
+
+## Test sending a message
+
+The easiest way to test sending a message is to see [the event emitted by `L2ToL2CrossDomainMessenger.sendMessage`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L160).
+
+To see this in action, run these commands:
+
+```sh
+cd testing/forge
+forge test GreetingSender --fork-url https://interop-alpha-0.optimism.io
+```
+
+The default [`anvil`](https://book.getfoundry.sh/anvil/overview) instance created by `forge test` does not contain the interop contracts, so we need to fork [a blockchain that does](/interop/tools/devnet).
+
+The test is implemented by `tests/GreetingSender.t.sol`.
+
+
+ Explanation
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L109-L117 hash=e8c77299b01dd6f03d3e36d2e9d82608
+ ```
+
+ The definition for the event we expect to see, [from the `L2ToL2CrossDomainMessenger` source code](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L160).
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L119-L121 hash=645c522fafccb282df37b7d3df27bd3f
+ ```
+
+ Create a new `GreetingSender` instance for each test.
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L124-L129 hash=5e79ce67c4e5f54eee5b8d09ace39860
+ ```
+
+ Calculate the message to be sent, the same way that `GreetingSender` does.
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L132-L132 hash=028411291cccc262557abde834549fc2
+ ```
+
+ Out of the indexed topics, verify the first (destination chain) and second (address on the destination chain).
+ Ignore the third topic, the nonce, because it can vary.
+ Finally, verify that the unindexed data of the log entry (the sender address and the message) is correct.
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L133-L133 hash=37203df9264e95e763befaf2e5dbbfab
+ ```
+
+ Emit the message we expect to see.
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L135-L135 hash=9322933b2f965ddc75bb8d3b2ab4c95d
+ ```
+
+ Call the code being tested, which should emit a similar log entry to the one we just emitted.
+
+
+## Test receiving a message
+
+To simulate receiving a message, we need to ensure two conditions are fulfilled:
+
+* The tested code is called by [`L2ToL2CrossDomainMessenger`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol).
+* The tested code can receive additional information through simulations of:
+ * [`L2ToL2CrossDomainMessenger.crossDomainMessageSender`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L102-L108)
+ * [`L2ToL2CrossDomainMessenger.crossDomainMessageSource`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L110-L116)
+ * [`L2ToL2CrossDomainMessenger.crossDomainMessageContext`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L118-L126)
+
+To see this in action, run these commands:
+
+```
+cd testing/forge
+forge test Greeter
+```
+
+The test is implemented by `tests/Greeter.t.sol`.
+
+
+ Explanation
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L171 hash=d9bb7b8ca88e8856bd86c256934a0b0b
+ ```
+
+ This function sets up the environment for a pretend interop call.
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L172-L182 hash=65aae51c8573ecb296b942c7073a3379
+ ```
+
+ Use [`vm.mockCall`](https://book.getfoundry.sh/reference/cheatcodes/mock-call) to specify the responses to calls to `L2ToL2CrossDomainMessenger`.
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L183-L187 hash=478229c219493adbbfea088cf266dec5
+ ```
+
+ At writing `crossDomainMessageContext` is not available in the [contracts npm package](https://www.npmjs.com/package/@eth-optimism/contracts-bedrock), so we calculate the selector directly using `bytes4(keccak256("crossDomainMessageContext()"))`.
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L188-L189 hash=18498ce1031cf749a13f6207d0828ca7
+ ```
+
+ Use [`vm.prank`](https://book.getfoundry.sh/reference/cheatcodes/prank) to make it appear the tested code is called by `L2ToL2CrossDomainMessenger`.
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L191-L201 hash=035afa3348edfa18a0be06b9f9991c38
+ ```
+
+ Test how `Greeter` acts when the greeting is set from another chain.
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L203-L210 hash=ea83b0fcf27566b5ae9221d1091bfc6b
+ ```
+
+ Test how `Greeter` acts when the greeting is set from this chain.
+
+
+## Next steps
+
+* Write a revolutionary app that uses multiple blockchains within the Superchain.
+* Write tests to make sure it works correctly.
diff --git a/pages/interop/tutorials/unit-tests/hardhat.mdx b/pages/interop/tutorials/unit-tests/hardhat.mdx
new file mode 100644
index 000000000..1a58bc7c8
--- /dev/null
+++ b/pages/interop/tutorials/unit-tests/hardhat.mdx
@@ -0,0 +1,185 @@
+---
+title: Interop and unit tests using Hardhat
+description: Learn how to write unit tests for interop when using Hardhat
+lang: en-US
+content_type: tutorial
+topic: interop-tutorial-unit-tests-hardhat
+personas:
+ - app-developer
+categories:
+ - protocol
+ - interoperability
+ - cross-chain-messaging
+ - tutorial
+ - testing
+ - hardhat
+is_imported_content: 'false'
+---
+
+import { Callout, Steps } from 'nextra/components'
+import { InteropCallout } from '@/components/WipCallout'
+
+
+
+# Interop and unit tests using Hardhat
+
+Most parts of the contracts can be [tested normally](/app-developers/testing-apps).
+This tutorial teaches you how to verify that a message has been sent successfully and to simulate receiving a message, the two functions that tie directly into interop.
+To show how this works, we test [the `Greeter` and `GreetingSender` contracts](/interop/tutorials/message-passing).
+
+## Setup
+
+This script creates a Foundry project (in `testing/forge`) and a Hardhat project (in `testing/hardhat`) with the necessary files.
+Execute it in a UNIX or Linux environment.
+
+The results of this step are similar to what the [message passing tutorial](/interop/tutorials/message-passing) produces.
+
+```sh file=/public/tutorials/setup-for-testing.sh hash=4d0fa175564131911ab6a12fb110294c
+```
+
+## Test sending a message
+
+The easiest way to test sending a message is to see [the event emitted by `L2ToL2CrossDomainMessenger.sendMessage`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L160).
+
+To see this in action, run these commands:
+
+```sh
+cd testing/hardhat
+npx hardhat test test/GreetingSender.js
+```
+
+Of course, Hardhat does not include the interop contracts by default.
+To have `L2ToL2CrossDomainMessenger.sendMessage` we fork [a blockchain that does have it](/interop/tools/devnet).
+This is specified in `hardhat.config.js`.
+
+The test is implemented by `test/GreetingSender.js`.
+
+
+ Explanation
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L258-L264 hash=3f669191f745ca77f1186d5d6b8dd062
+ ```
+
+ Deploy a `GreetingSender`.
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L266-L270 hash=40785947670a88582c43f31e37d7cb28
+ ```
+
+ Create a contract object for `L2ToL2CrossDomainMessenger`.
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L280-L286 hash=80f81c9dafbea8a958f0bfacb12e9389
+ ```
+
+ Calculate the calldata that will be sent by `L2ToL2CrossDomainMessenger`.
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L288 hash=ea3c2728d499ef1536178d53fdb5497a
+ ```
+
+ The operation we're testing, `greetingSender.setGreeting(greeting)`.
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L289 hash=76eeeefe7b7605474ab8e43105e7dc0d
+ ```
+
+ The expected result, to have the `messenger` contract emit a `SentMessage` log entry.
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L290-L296 hash=14bf71dc909944de148fda7bcc4d0f2d
+ ```
+
+ The parameters in the log entry.
+ We use `anyValue` for the nonce because we do not know in advance what value it would be.
+
+
+## Test receiving a message
+
+To simulate receiving a message, we need to ensure two conditions are fulfilled:
+
+* The tested code is called by [`L2ToL2CrossDomainMessenger`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol).
+* The tested code can receive additional information through simulations of:
+ * [`L2ToL2CrossDomainMessenger.crossDomainMessageSender`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L102-L108)
+ * [`L2ToL2CrossDomainMessenger.crossDomainMessageSource`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L110-L116)
+ * [`L2ToL2CrossDomainMessenger.crossDomainMessageContext`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L118-L126)
+
+To see this in action, run these commands:
+
+```
+cd testing/hardhat
+npx hardhat test test/Greeter.js
+```
+
+This test is implemented by two files, `contracts/MockL2ToL2Messenger.sol` which replaces `L2ToL2CrossDomainMessenger` with a contract we control, and `test/Greeter.js` which actually has the tests.
+
+
+ Explanation of `contracts/MockL2ToL2Messenger.sol`
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L374-L380 hash=870b143463550d8f0915a4db9d584cea
+ ```
+
+ When the tested code receive a message, these are the source chain and address where it originates.
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L382-L392 hash=7f9cf709fa17ce391ec40728267b82a0
+ ```
+
+ These are the three functions we need to implement for tested code, such as `Greeter`, to call.
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L394-L408 hash=13b29fd8bbcccfc6c7a5201f5d316d0a
+ ```
+
+ Because we are replacing the `L2ToL2CrossDomainMessenger` with this contract, and the order in which tests are executed is not deterministic, we need this contract to work for the `GreetingSender` test.
+ The code here is copied from [`L2ToL2CrossDomainMessenger.sendMessage`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L128-L161), with the irrelevant parts removed.
+
+ ```solidity file=/public/tutorials/setup-for-testing.sh#L410-L413 hash=0797417fc01fcc3a27511f55729e1cbe
+ ```
+
+ The interop message `Greeter` gets has to come from the `L2ToL2CrossDomainMessenger` address.
+ We can either send a transaction into `MockL2ToL2Messenger` that then calls `Greeter`, or give the `L2ToL2CrossDomainMessenger` address some ETH and send the transaction directly from it.
+ This function enables us to give the contract ETH.
+
+
+
+ Explanation of `test/Greeter.js`
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L310 hash=6fd176fc44208d2f92f0210015c8e806
+ ```
+
+ This function deploys the fixtures we need to run the tests.
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L312-L319 hash=7d218ce8fead9bc8412e79303d026c16
+ ```
+
+ This is the code that first deploys `MockL2ToL2Messenger` and then tells Hardhat to direct calls to the `L2ToL2CrossDomainMessenger` contract to it instead.
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L321-L322 hash=b6df4166a44f7b5f006d269f97a4e34a
+ ```
+
+ Deploy a `Greeter` to test.
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L324-L328 hash=ee7023cfaf1b9672914aa77b75e77285
+ ```
+
+ Create a contract object for `L2ToL2CrossDomainMessenger`.
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L334-L340 hash=d44100c903a4fd42aedadce81455226a
+ ```
+
+ Run the test for local messages.
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L347-L352 hash=b37199ac43cee36514ab508d763049f8
+ ```
+
+ Create an object for `MockL2ToL2Messenger` and give it some ETH to be able to send transactions.
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L354-L358 hash=7ae6ae561d95426670ea1d0311bc9711
+ ```
+
+ Test that when receiving an interop message, `Greeter` still emits `SetGreeting` correctly.
+
+ ```js file=/public/tutorials/setup-for-testing.sh#L360-L364 hash=e0b49ce1ca70cc58378abcfba8d1d2ca
+ ```
+
+ Test that when receiving an interop message, `Greeter` also emits `CrossDomainSetGreeting`.
+
+
+## Next steps
+
+* Write a revolutionary app that uses multiple blockchains within the Superchain.
+* Write tests to make sure it works correctly.
diff --git a/public/tutorials/setup-for-testing.sh b/public/tutorials/setup-for-testing.sh
new file mode 100644
index 000000000..9580c138c
--- /dev/null
+++ b/public/tutorials/setup-for-testing.sh
@@ -0,0 +1,415 @@
+#! /bin/sh
+
+rm -rf testing
+mkdir -p testing/forge
+cd testing/forge
+
+forge init
+find . -name 'Counter*' -exec rm {} \;
+cd lib
+npm install @eth-optimism/contracts-bedrock
+cd ..
+echo @eth-optimism/=lib/node_modules/@eth-optimism/ >> remappings.txt
+
+cat > src/Greeter.sol < src/GreetingSender.sol < test/GreetingSender.t.sol < test/Greeter.t.sol < contracts/GreetingSender.sol
+find . -name 'Lock*' -exec rm {} \;
+npm install @eth-optimism/contracts-bedrock dotenv @eth-optimism/viem
+
+cat > hardhat.config.js < test/GreetingSender.js < {
+
+ const GreetingSender = await ethers.getContractFactory(
+ "GreetingSender"
+ )
+ const greetingSender = await GreetingSender.deploy(
+ targetGreeter,
+ targetChain
+ )
+
+ const messenger = new ethers.Contract(
+ contracts.l2ToL2CrossDomainMessenger.address,
+ l2ToL2CrossDomainMessengerAbi,
+ ethers.provider
+ );
+
+ return { greetingSender, messenger };
+ }
+
+ it("emits SentMessage with the right arguments", async () => {
+ const { greetingSender, messenger } = await deployFixture()
+
+ const greeting = "Hello"
+
+ // build the exact calldata the test expects
+ const iface = new ethers.Interface([
+ "function setGreeting(string)",
+ ])
+ const calldata = iface.encodeFunctionData("setGreeting", [
+ greeting,
+ ])
+
+ await expect(greetingSender.setGreeting(greeting))
+ .to.emit(messenger, "SentMessage")
+ .withArgs(
+ targetChain,
+ targetGreeter,
+ anyValue,
+ greetingSender.target,
+ calldata
+ )
+ })
+})
+EOF
+
+cat > test/Greeter.js < {
+ const fakeSender = "0x0123456789012345678901234567890123456789";
+ const fakeSourceChain = 901;
+
+ async function deployFixture() {
+
+ const MockMessenger = await ethers.getContractFactory("MockL2ToL2Messenger");
+ const mock = await MockMessenger.deploy(fakeSender, fakeSourceChain);
+
+ // overwrite predeploy with mock code
+ await network.provider.send("hardhat_setCode", [
+ contracts.l2ToL2CrossDomainMessenger.address,
+ await ethers.provider.getCode(mock.target),
+ ]);
+
+ const Greeter = await ethers.getContractFactory("Greeter");
+ const greeter = await Greeter.deploy();
+
+ const messenger = new ethers.Contract(
+ contracts.l2ToL2CrossDomainMessenger.address,
+ MockMessenger.interface,
+ ethers.provider
+ );
+
+ return { greeter, messenger, mockMessenger: mock };
+ }
+
+ it("emits SetGreeting with the right arguments when called locally", async () => {
+ const { greeter } = await deployFixture();
+ const greeting = "Hello";
+
+ await expect(greeter.setGreeting(greeting))
+ .to.emit(greeter, "SetGreeting")
+ .withArgs((await ethers.getSigners())[0].address, greeting);
+ });
+
+ it("emits SetGreeting and CrossDomainSetGreeting with the right arguments when called remotely",
+ async () => {
+ const { greeter } = await deployFixture();
+ const greeting = "Hello";
+
+ const impersonatedMessenger =
+ await ethers.getImpersonatedSigner(contracts.l2ToL2CrossDomainMessenger.address);
+ const tx = await (await ethers.getSigners())[0].sendTransaction({
+ to: contracts.l2ToL2CrossDomainMessenger.address,
+ value: ethers.parseEther("1.0")
+ });
+
+ await expect(
+ greeter.connect(impersonatedMessenger).setGreeting(greeting)
+ )
+ .to.emit(greeter, "SetGreeting")
+ .withArgs(contracts.l2ToL2CrossDomainMessenger.address, greeting);
+
+ await expect(
+ greeter.connect(impersonatedMessenger).setGreeting(greeting)
+ )
+ .to.emit(greeter, "CrossDomainSetGreeting")
+ .withArgs(fakeSender, fakeSourceChain, greeting);
+ });
+});
+EOF
+
+cat > contracts/MockL2ToL2Messenger.sol <