TestEZ

repository·master·Indexed 42 days ago

https://github.com/roblox/testez

A BDD-style testing framework for Roblox Lua featuring describe, it, and expect syntax. It supports in-game testing and CI environments via Lemur, and includes a CLI (v0.3.0) for test execution and management.

Tokens
5K
Snippets
16
Records
24
Agent score
49%

What's inside TestEZ

  1. Overview of TestEZ testing framework

    master

    TestEZ is a Behavior-Driven Development (BDD) style testing framework designed specifically for Roblox Lua. It is used for testing applications, in-game core scripts, Roblox Studio plugins, and various Roblox libraries (such as Roact and Rodux).

    TestEZ offers two primary ways to interact with it:

    1. A high-level API that allows running all tests with a single method call.
    2. A granular API that exposes each individual step of the testing pipeline for more control.
  2. Overview of TestEZ

    master

    TestEZ is a Behavior-Driven Development (BDD) style testing framework for Roblox Lua. It is designed to run within the Roblox engine and can also be used inside Lemur for testing on CI systems.

    TestEZ provides two main ways to interact with tests:

    1. A high-level API to run all tests with a single method call.
    2. A granular API that exposes each step of the testing pipeline for more control.
  3. Implement a custom Test Reporter

    master

    Reporting is pluggable. To create a custom reporter, implement a table that contains a report method. This method must accept a TestResults object as its argument.

    local myReporter = {
        report = function(results) 
            -- logic to output results to stdout, file, etc.
        end
    }
  4. TestEZ Syntax and Methodology

    master

    TestEZ follows BDD (Behavior-Driven Development) principles, utilizing familiar syntax patterns:

    • describe and it: Used to structure test suites and individual test cases. This syntax is inspired by RSpec, busted, Mocha, and Ginkgo.
    • expect: Used for assertions. The expectation syntax is based on Chai.
  5. Project requirements for TestEZ CLI

    master

    TestEZ CLI is designed specifically for library or plugin-type projects. It requires a specific project structure to function correctly:

    • Source Directory: Your source code must reside in a folder named src.
    • Dependencies: Dependencies must be located in either:
      • Git submodules within a modules/ folder.
      • Rotriever dependencies within a Packages/ folder.
    • Rojo Configuration: If your project contains a default.project.json, it must be configured to build your library, not a test place.
    • Runtime Requirements:
      • For Lemur target: lua must be on your PATH and Lemur's dependencies must be installed. (Using hererocks is recommended for Lua management).
      • For Roblox-CLI target: roblox-cli and rojo must be on your PATH.
  6. Use describe, it, and expect to structure tests

    master

    TestEZ uses a hierarchical structure to organize tests:

    • describe(name, callback): Groups related tests together. The name provided helps define the test scope in the output.
    • it(name, callback): Defines an individual test case. The callback contains the actual logic and assertions.
    • expect(actual): The entry point for assertions. It returns an expectation object that you chain with matchers.

    Tests are implicitly scoped according to their file path, creating a tree structure in the test report.

    -- Example of a structured spec
    return function()
    	local MyModule = require(script.Parent.MyModule)
    
    	describe("MyModule:methodName", function()
    		it("should perform expected behavior", function()
    			local result = MyModule:methodName()
    			expect(result):to.be.ok()
    		end)
    	end)
    end
  7. Use lifecycle hooks for setup and teardown

    master

    Lifecycle hooks allow you to collect setup and teardown code into a common place. They can be used within a describe block or in an init.spec.lua file to apply to all tests in a folder.

    Note: Callbacks in lifecycle hooks run after all describe block callbacks. Do not rely on them for code that must run before describe blocks.

    HookExecution TimingUse Case
    beforeAll(callback)Before any tests in scope runOne-time setup for a group
    afterAll(callback)After all tests in scope runGlobal cleanup
    beforeEach(callback)Before every single testResetting state for isolation
    afterEach(callback)After every single testCleaning up temporary state

    Example: Resetting state with beforeEach

    local globalState = {}
    
    beforeEach(function()
    	globalState.foo = 100
    end)
    
    it("SHOULD be able to read foo", function()
    	expect(globalState.foo).to.equal(100)
    end)
    
    it("SHOULD be able to write foo", function()
    	globalState.foo = globalState.foo / 2
    	expect(globalState.foo).to.equal(50)
    end)
    local globalState = {}
    
    beforeEach(function()
    	globalState.foo = 100
    end)
    
    it("SHOULD be able to read foo", function()
    	expect(globalState.foo).to.equal(100)
    end)
  8. Organize tests with describe and it

    master

    TestEZ uses a hierarchical structure to organize tests:

    • describe(phrase, callback): Creates a block representing a thing being tested (e.g., a class or module). Use this to group related behaviors.
    • it(phrase, callback): Creates a block representing a specific behavior expected of the thing. These should be placed inside describe blocks.

    Example structure:

    describe("This cheese", function()
    	dit("should be moldy", function()
    		expect(cheese.moldy).to.equal(true)
    	end)
    end)
    describe("This cheese", function()
    	it("should be moldy", function()
    		expect(cheese.moldy).to.equal(true)
    	end)
    end)
  9. How the TestEZ testing lifecycle works

    master

    TestEZ follows a four-step lifecycle to manage tests from discovery to reporting. This separation of concerns allows you to inspect or modify the test structure before execution and provides pluggable reporting mechanisms.

    1. Load tests: Automatically discovers ModuleScript objects with names ending in .spec (e.g., MyModule.spec.lua). These modules must return a function that uses describe, it, and expect.
    2. Create test plan: A TestPlanner uses a TestPlanBuilder to construct a tree of all describe and it calls without actually executing the test code. This allows for inspecting the test tree or selecting specific tests to run.
    3. Execute test plan: A TestRunner takes the TestPlan and creates a TestSession to execute the tests, producing a TestResults object that mirrors the plan's structure.
    4. Report results: A reporter object (a table with a report method) consumes the TestResults to output data to stdout, a GUI, or TestService.
    -- Example of the expected test structure in a .spec module
    return function()
    	describe("Addition", function()
    		it("should be commutative", function()
    			local a, b, c = 5, 8, 11
    			expect(a + b + c).to.equal(c + b + a)
    		end)
    
    		it("should be associative", function()
    			local a, b, c = 7, 4, 9
    			expect((a + b) + c).to.equal(a + (b + c))
    		end)
    	end)
    end
  10. Focus or skip tests during development

    master

    To isolate specific tests while working on code, TestEZ provides FOCUS() and SKIP() functions. These allow you to run only the code blocks you are currently interested in or to bypass specific blocks entirely.

    Using FOCUS() and SKIP() in describe blocks

    You can call FOCUS() or SKIP() directly inside a describe block. Because describe blocks are executed immediately to build the test tree, these calls will affect all tests contained within that block.

    Using itFOCUS and itSKIP for it blocks

    FOCUS() and SKIP() do not work inside it blocks because the code inside an it block is not executed until the test runner actually runs the tests. To focus or skip a specific test case, use the specialized itFOCUS and itSKIP functions instead.

    !!! warning FOCUS and SKIP are intended exclusively for development. Do not check tests containing these calls into version control. Future versions of TestEZ may detect these in CI environments and fail the build to prevent accidental commits.

    return function()
    	describe("new", function()
    		FOCUS()
    
    		it("does really important things", function()
    			-- This block will run!
    		end)
    	end)
    
    	itFOCUS("has all methods we expect", function()
    		-- Calling FOCUS() would be too late here, so we use itFOCUS instead.
    
    		-- This block will run, too
    	end)
    
    	describe("Format()", function()
    		it("formats things", function()
    			-- This block will never run!
    		end)
    	end)
    end
  11. Run tests with TestEZ CLI

    master

    Once your project structure and environment are configured, use the testez run command with a --target flag to execute your tests.

    Using Lemur (Default)

    Use this target if you have lua and Lemur dependencies installed. This is the default behavior.

    testez run --target lemur

    Using Roblox-CLI

    Note: This target is currently for Roblox internal use only.

    testez run --target roblox-cli
    # Using Lemur (default)
    testez run --target lemur
    
    # Using Roblox-CLI (Roblox internal only for now)
    testez run --target roblox-cli