Skip to content

Instantly share code, notes, and snippets.

@shanecelis
Last active July 15, 2022 15:03
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shanecelis/d33c5f0ae2f6f8337a50 to your computer and use it in GitHub Desktop.
Save shanecelis/d33c5f0ae2f6f8337a50 to your computer and use it in GitHub Desktop.
I was curious about how one could manually drive Unity's coroutines without necessarily putting them into Unity's scheduler.
/*
CoroutineTests.cs -- Shane Celis
I was curious about how one could manually drive Unity's coroutines
without necessarily putting them into Unity's scheduler. At the
heart of it, it's really easy to manually drive them:
// Get the coroutine.
IEnumerator ienum = MyCoroutine();
// Run it until it yields.
ienum.MoveNext();
// Get its return value;
Object result = ienum.Current;
// Exhaust it.
while (ienum.MoveNext())
;
However, to fully inspect how this machinery is all setup I wrote
these tests.
This article was pretty helpful:
"When a routine contains a yield then the compiler starts to do
some magic. That coroutine looks like a normal function in your
class doesn't it? Well it isn't that at all. Behind the curtain
the compiler has constructed a dummy class which implements
IEnumerator, this class has its own fields, one field for every
local variable you have defined in your function and another to
reference the instruction that last executed.
"When that magic class's IEnumerator.MoveNext() is called your
code starts to run and the compiler automagically gives you the this
pointer for your classes instance, rather than the one for the magic
class - it also works out how to reference the variables in your
function. In other words you've built a state machine disguised as a
function" [1].
[1]: http://unitygems.com/advanced-coroutines/
*/
using System;
using System.Collections;
using NUnit.Framework;
using UnityEngine;
[TestFixture]
internal class CoroutineTests
{
int state = 0;
/* Simple coroutine that let's us inspect what state it was in and
varying return values. */
private IEnumerator TestCoroutine() {
state = 1;
yield return 0;
state = 2;
yield return "a"; // Notice that the return type is not strictly
// one type.
state = 3;
yield return 2;
state = 4;
}
/*
Let's just drive this coroutine to completion and inspect its
values as we go.
*/
[Test]
public void ManuallyDriveCoroutineTest ()
{
Assert.AreEqual(0, state);
IEnumerator ienum = TestCoroutine();
Assert.AreEqual(0, state); // The coroutine hasn't run yet.
Assert.IsTrue(ienum.MoveNext());
Assert.AreEqual(0, ienum.Current);
Assert.AreEqual(1, state);
Assert.IsTrue(ienum.MoveNext());
// We can return whatever kind of object we like.
Assert.AreEqual("a", ienum.Current);
Assert.AreEqual(2, state);
Assert.IsTrue(ienum.MoveNext());
Assert.AreEqual(2, ienum.Current);
Assert.AreEqual(3, state);
Assert.IsFalse(ienum.MoveNext());
// The current value remains the same as from the last call.
Assert.AreEqual(2, ienum.Current);
Assert.AreEqual(4, state);
}
private IEnumerator NestedCoroutine() {
yield return TestCoroutine();
yield return 3;
}
/* Nested coroutines can be returned but they'd need to be run
* manually if you're opting out of Unity's scheduler. */
[Test]
public void TestNestedCoroutine() {
IEnumerator ienum = NestedCoroutine();
Assert.IsTrue(ienum.MoveNext());
Assert.IsInstanceOf<IEnumerator>(ienum.Current);
Assert.AreEqual(0, state); // ienum has not been run.
Assert.IsTrue(ienum.MoveNext());
Assert.AreEqual(3, ienum.Current);
Assert.IsFalse(ienum.MoveNext());
Assert.AreEqual(3, ienum.Current);
}
/* This coroutine will pass thru another coroutine it calls. */
private IEnumerator PassThruCoroutine(bool passThru) {
IEnumerator ienum = TestCoroutine();
if (passThru) {
while (ienum.MoveNext()) {
yield return ienum.Current;
}
} else {
yield return ienum;
}
yield return 4;
}
/* This coroutine will manually run a nested coroutine so that it
all looks like just one big coroutine to the caller. */
[Test]
public void TestPassThruCoroutine() {
IEnumerator ienum = PassThruCoroutine(true);
int count = 0;
while (ienum.MoveNext()) {
count++;
}
Assert.AreEqual(4, count);
ienum = PassThruCoroutine(false);
count = 0;
while (ienum.MoveNext()) {
count++;
}
Assert.AreEqual(2, count);
}
/* We can't reset coroutines, which is just as well. */
[Test]
[ExpectedException (typeof (NotSupportedException))]
public void CoroutinesCantReset() {
IEnumerator ienum = TestCoroutine();
ienum.Reset();
}
[SetUp]
public void SetUp() {
state = 0;
}
[TearDown]
public void TearDown() {
state = 0;
}
}
@paulhocker
Copy link

Genius - thanks for sharing this - I have been trying to figure out how to test coroutines, this looks like the ticket.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment