Using .spec files with TestEZ // Debug and test your code like Roblox does!

Introduction

I want to start off strong by having you try something in Roblox Studio.

  • Create a script
  • Put .spec at the end of the script name

Now try typing “describe.” Notice how the function name autofills, despite it not being explicitly defined. Why?

This is because internally, Roblox uses .spec files to test and debug code. This code is run with TestEZ, and is used as a BDD-style testing framework.

TestEZ is used for testing apps, in-game core scripts, built-in Roblox Studio plugins, and libraries such as Roact and Rodux. It provides an API that can run all tests with a single method call as well as a more granular API that exposes each step of the pipeline.

How do I use TestEZ?

Rather than break it down here, I recommend you read the official documentation on it!

https://roblox.github.io/testez/

How do I download TestEZ?

You could always visit the source code and port it to Roblox yourself. Don’t worry though, I have a Roblox model you can use.

TestEZ.rbxm (19.3 KB)

Download this module and put it in a spot where it’s easily accessible to your code.

Using TestEZ with .spec files

Now for the part you want, the actual walkthrough!

For the sake of the following examples, I have both TestEZ and a test script located in ServerScriptService.

image

In TestScript, I want to make an example module, as well as a .spec file for it. I’m going to name the module “DevforumExample,” and will name the spec file “DevforumExample.spec.”

image

First in the TestScript, I will declare both TestEZ and DevforumExample. I will NOT declare DevforumExample.spec. Attempting to run a spec file outside of the TestEZ environment will result in errors.

image

Now, for example’s sake, I will make some example code in the DevforumExample module. It looks like this:

image

Now we can open the spec file for this module. Let’s start by making it return a function instead of a dictionary.

image

We will use this spec file to describe what actions are appropriate with each function. Let’s start by defining what behavior should be explicit with DevforumExample.foo()

Before this, we have to define the DevforumExample module in the spec file.

image

Now let’s use it. This function creates a new ‘it’ block. These blocks correspond to the behaviors that should be expected of the thing you’re testing.

In this case, let’s expect DevforumExample.foo() to return a string.

2JHjZ8dr

What you put in the first parameter doesn’t affect behavior, and is simply used for you to document what should happen.

However, we do still need the machine to understand that the function should a string. To do this, we use expect. This creates a new Expectation, used for testing the properties of the given value.

Expectations are intended to be read like English assertions. These are all true:

-- Equality
expect(1).to.equal(1)
expect(1).never.to.equal(2)

-- Approximate equality
expect(5).to.be.near(5 + 1e-8)
expect(5).to.be.near(5 - 1e-8)
expect(math.pi).never.to.be.near(3)

-- Optional limit parameter
expect(math.pi).to.be.near(3, 0.2)

-- Nil checking
expect(1).to.be.ok()
expect(false).to.be.ok()
expect(nil).never.to.be.ok()

-- Type checking
expect(1).to.be.a("number")
expect(newproxy(true)).to.be.a("userdata")

-- Function throwing
expect(function()
    error("nope")
end).to.throw()

expect(function()
    -- I don't throw!
end).never.to.throw()

expect(function()
    error("nope")
end).to.throw("nope")

expect(function()
    error("foo")
end).never.to.throw("bar")

Since we need to expect foo() to return a string, let’s explicitly define that!

FB2ZZXmZ

Let’s go back to the main TestScript script. In there, we will use TestEZ to run this code.

DZM5niIK

We use run to run tests in a containing bin. The first parameter is the bin of modules, and the second parameter is the callback function, which has a result parameter. For testing sake, we will just print the result.

If we run the experience, we get this in the output:

image

Notice how there are no errors, and there is one success. This means the test we made in the spec file runs as expected. Below is an implementation for the rest of the functions:

image

Now how do we know when something does not function as intended?

Let’s assume that we broke DevforumExample.add()

image

If we click run again, we get the following result:

image

If we expand the errors index, we can see very clearly what went wrong:

image

Pay attention to the top line of the stack, which reads:
"ServerScriptService.TestScript.DevforumExample.spec:16: Expected value "24" (number), got "0" (number) instead

You can use information like this to fix code that doesn’t function as intended!

Concluding Statements

I created this tutorial simply because I discovered that .spec was used in places such as Roact, but I had no idea what they did and could not find anything on the internet about it. Big shoutout to @boatbomber for helping me find out what they do and how to run them.

I would love to hear what everybody thinks of Roblox’s system of internal testing. Are spec files and TestEZ the way to test, or do you have something that is better?

Thanks for the read, have a good day!

28 Likes

It’s worth noting that you should never include TestEZ in your game, as it uses getfenv() and setfenv(), which remove a ton of Luau optimizations. TestEZ should be a dev-dependency, which you can set up with Rojo and some CI scripts.

As far as I know, TestEZ is the only testing framework on Roblox, which kind of sucks because it’s designed for BDD and it uses setfenv()/getfenv() :confused: (not that bdd is necessarily a bad thing, it’s just that bdd typically isn’t what most devs need)

I plan on making a testing framework Someday :tm:, but someone will probably beat me to it.

1 Like

couldn’t you also use TDD using TestEZ? Ultimately, both BDD and TDD are just practices.

TestEZ was designed with bdd in mind, that’s why I said that. It was probably ambiguous of me to say that, so it’s really my bad. But specifically because TestEZ is designed for bdd, it uses getfenv() and setfenv()- hence why I said you shouldn’t include it in your game.

Sorry for the bad wording, that’s on me.

2 Likes