Setting Up
In this tutorial we'll walk through setting up an Autometa based API testing framework in a Typescript project. The goal of this guide is to end up with a project that can run tests against a local server, or a live deployed service.
From Autometa we'll use the following libraries:
@autometa/runner- Our Cucumber executor, which contains a HTTP Client built on Axios@autometa/status-codes- A collection of HTTP status codes and their descriptions which can be used for assertions.@autometa/builder- Simply define and create DTOs and Request obects using a builder pattern.
We'll also use some other great libraries:
- MyZod, Zod or other schema
validation library you like to validate our API responses and reduce our 'Testing Surface Area'
- Schemas let us validate the "shape" of a response. e.g is 
nameastringanddetailsanobject? - Validators besides MyZod and Zod may require you to write a wrapper to interact with the Autometa HTTP Client, if in use.
 
 - Schemas let us validate the "shape" of a response. e.g is 
 - Envalid a schema validator for the
environment variables in our project. You could also make your own with MyZod or Zod.
- This will parse our 
process.envincluding values injected from CI Workflows or.envfiles. - Handles type conversion from 
string(only type supported byprocess.env) tonumber,boolean,objectetc. 
 - This will parse our 
 - DotEnv to load our environment variables from a 
.envfile- We'll store our API urls and other environment-based or sensitive data in this file (do not commit this file to source control)
 
 - Reflect Metadata - To allow us to use decorators in Typescript
- This requires 
experimentalDecoratorsandemitDecoratorMetadatato be set totruein yourtsconfig.json 
 - This requires 
 - Jest - At present, Autometa requires Jest as a Test Runner. Future support for Vitest is planned.
 - ts-jest - To allow Jest to run Typescript tests
 - ts-node - To allow Jest to run Typescript tests
 
Optional:
If you prefer the workflow of Axios or node-fetch you can use that as a HTTP client instead.
This library requires experimentalDecorators and emitDecoratorMetadata to be set to true in your tsconfig.json
Pre Setup
To begin, set up a new Typescript project to your preferred configuration. If starting from scratch, you can use a project template such as this.
Configure your jest.config & tsconfig to your liking and install the following dependencies:
npm i -D @autometa/runner @autometa/status-codes @autometa/builder myzod envalid dotenv
Our API
For this example we're going to build a framework to test the free Dummy JSON API.
Choose Your test Style
Autometa supports two styles of testing:
- Gherkin
- Uses the 
@autometa/jest-transformerto execute Cucumber.featurefiles directly in Jest. - Test Scenarios are assembled automatically by globally defined 
Step DefinitionsandHooks. 
 - Uses the 
 - Jest-Cucumber Inspired
- Executes code files which reference 
.featurefiles, and support nested Step Definitions, and concrete test scenarios. 
 - Executes code files which reference 
 
For this tutorial we'll use Gherkin style.
Create a Config file
In the Root of your project, create a file called autometa.config.ts and add the following:
import { defineConfig } from "@autometa/runner";
export default defineConfig();
Next we need to add the following options:
- runner - The Library or Framework running our tests
- Currently only 
jestis supported 
 - Currently only 
 - roots - roots define the roots for important files in our project.
features- The root of our.featurefiles relative to the project root.- e.g. 
$root/integration/features 
- e.g. 
 - steps: The root of our 
Step Definitionsrelative to the project root.- e.g. 
$root/integration/steps 
 - e.g. 
 - Step Definitions act as "import with side effects". This option is required to ensure they are loaded.\
 - app: The 'App' is the entry point for our tests and a deviation from Cucumbers default "World" concept. It is the file that will be executed by our test runner.
- e.g. 
$root/integration/app.ts - A class 
Appcontains our supporting and utility classes via dependency injection. - A class 
Worldcontains our test state and is passed to each test scenario. Almost identitical to Cucumber'sWorldconcept. - (recommended) a 
env.tsfile to contain our environment variables and their types using Envalid. 
 - e.g. 
 
 - (Optional) shim - an object which can enable or disable shims. Currently only 'Error Causes' is supported.
- Error Causes - Errors in jest will contain a stack of errors, not just a textual stack trace.
 - Not necessary for ecmascript 2022+ as it's now supported natively
 
 
import { defineConfig } from "@autometa/runner";
export default defineConfig({
  runner: "jest",
  roots: {
    features: "integration/features",
    steps: "integration/steps",
    app: "src/app.ts"
  },
  shim: {
    errorCauses: true
  }
});
Make sure your autometa.config.ts is included in your tsconfig file under include or files:
{
  "include": ["autometa.config.ts"]
}
App
The App is our central point of communication within a test. An App is a class you define in your Framework
using the @AppType decorator. AppType takes a World argument. This is reference to your World class.
In the official Cucumber implementation, there is no App concept. Instead, Cucumber relies on implicit
access to the World object through the this variable. As a result, Cucumber functions must be defined with
function(){} syntax, and cannot use (fat)=> 'arrow' functions.
In Cucumber this is bound to the tests World object.
In Autometa the World is defined explicitly, and it is a child of the App. Conceptually,
the World is a state manager. It acts as a way of passing state between steps within a
Scenario's Step Definitions. It is unique between tests but shared between steps.
The App is a wrapper over the world, which represents state, and other fixtures which handle behavior,
such as HTTP Clients, Database Clients, Page Objects or other utilities.
We can add dependencies to the app by creating classes marked with @Fixture:
// my-client.ts
import { Fixture, Constructor HTTP } from "@autometa/runner";
@Fixture
@Constructor(HTTP)
export class MyClient {
  constructor(readonly http: HTTP) {
    this.http.url(Env.API_URL);
  }
  async getResource() {
    return await this.http.route("myResource").get();
  }
}
// app.ts
import { AppType } from "@autometa/runner";
import { MyClient } from "./my-client";
import { World } from "./world";
@AppType(World)
export class App {
  constructor(readonly myClient: MyClient) {}
}
HTTP is a built in HTTP client which wraps Axios.
The app will be instantiated once per test and will contain a test-specific reference to a World instance.
import { Given } from "@autometa/runner";
import { App } from "../app";
When("I retrieve the resource", ({ world, myClient }: App) => {
  world.response = await myClient.getResource();
});
It is not necessary to explicitely define the paramater as : APP provided you follow the steps Declaration Overrides
below. With overrides, the App is inferred and the World with it
World
World is a Key:Value store represented by a blank class instance. It is automatically injected into the App during tests.
Values in the world will persist between
- Before Hooks
 - After Hooks
 - Scenarion and Scenario Outline Step Definitions
 - Background Step Definitions
 
Meaning you can set up data in a pretest hook and use it as seed data for your tests. We can declare values on the world which are undefined by default, but are available with their types until the value is filled.
import { Fixture, HTTPResponse } from "@autometa/runner";
import { MyResourceBody } from "./myclient/myclient.types.ts";
@Fixture
export class World {
  declare myResourceResponse: HTTPResponse<MyResourceBody>;
}
Env
To set up our environment variables we'll use Envalid and DotEnv.
Create a .env file in the root of your project and add the following:
API_URL=https://dummyapi.io/data/api
Next create a file env.ts in ./src/env of your project and add the following:
import { cleanEnv, str } from "envalid";
import { config } from "dotenv";
config();
export const Env = cleanEnv(process.env, {
  API_URL: str()
});
Here we defined an API_URL which points to our API. This can easily be configured from .env files,
or CI/CD workflows.
// some-file.ts
const url = Env.API_URL;
Declaration Overrides
Your App/World classes are unique. No other project has one quite like yours. It's important then
that it be declared in a way that is unique to your project. To do this, we'll use a declaration override.
Create a new directory __typings__ and include it as a typeRoot in your tsconfig.json:
Now that we have our App and World defined, we can declare them to override Autometas empty default interfaces.
{
  "compilerOptions": {
    "typeRoots": ["./__typings__"]
  }
}
Next create autometa.d.ts, and override Autometas internal App and World interfaces with your own:
import { App as MyApp, World as MyWorld } from "../src/app";
declare module "@autometa/runner" {
  interface App extends MyApp {}
  interface World extends MyWorld {}
}
Steps will now automatically infer the type of the App and World, and it is no longer necessary to explicitely define their type in your Step Definitions.
Given("I have a world", ({ world }) => {
  // world is inferred as MyWorld
});
We can also use these overridden to automatically infer the type of Cucumber Expressions.
Given("a {builder:product} to add", (product, { world }) => {
  // product is inferred as ProductBuilder
  // world is inferred as MyWorld
  world.productBuilder = product;
});
// src/app/types.ts
interface Types {
  "builder:product": ProductBuilder;
}
import type { App as MyApp, World as MyWorld } from "../src/app";
import type { ProductBuilder } from "../src/product/product.builder";
import type { Types as T } from '@autometa/runner';
declare module "@autometa/runner" {
  interface App extends MyApp {}
  interface World extends MyWorld {}
  interface Types extends T {}
}
Use the export * from './foo' syntax in index.ts files to make import your modules more easily.