CUTE HTTP and REST API Testing Framework for Go

repository·master·Indexed 42 days ago

https://github.com/ozontech/cute

An HTTP and REST API testing framework for Go featuring a BDD-like syntax and deep integration with Allure for rich reporting. It provides built-in JSONPath and header assertions, JSON schema validation, and support for table tests via builder or array patterns. CUTE allows for custom assertions at multiple levels of complexity and provides wrappers to mark assertions as required, optional, or broken.

Tokens
17.8K
Snippets
68
Records
95
Agent score
53%

What's inside CUTE

  1. Create custom asserts

    master

    CUTE allows implementing custom assertions at three different levels of complexity:

    1. Base Asserts

    Simple functions that return an error if the assertion fails. Use these for standard logic.

    • AssertBody func(body []byte) error
    • AssertHeaders func(headers http.Header) error
    • AssertResponse func(response *http.Response) error

    2. T Asserts (Allure & Testing.TB integration)

    These allow you to use cute.T to log information to Allure, create steps, and use require style assertions.

    • AssertBodyT func(t cute.T, body []byte) error
    • AssertHeadersT func(t cute.T, headers http.Header) error
    • AssertResponseT func(t cute.T, response *http.Response) error

    3. Custom Assert Errors

    To create detailed, pretty errors in Allure, use the cute/errors package:

    • errors.NewAssertError(name, message, params, ...): Creates a structured assertion error.
    • errors.NewOptionalError(err): Creates an error that causes the step to fail but allows the overall test to pass.
    // Example of a Base Assert
    func customAssertBody() cute.AssertBody {
        return func(bytes []byte) error {
            if len(bytes) == 0 {
                return errors.New("response body is empty")
            }
            return nil
        }
    }
    
    // Example of a T Assert with Allure steps
    func customAssertBodySuite() cute.AssertBodyT {
        return func(t cute.T, bytes []byte) error {
            step := allure.NewSimpleStep("Custom assert step")
            defer func() {
                t.Step(step)
            }()
    
            if len(bytes) == 0 {
                step.Status = allure.Failed
                step.Attachment(allure.NewAttachment("Error", allure.Text, []byte("response body is empty")))
                return nil
            }
            return nil
        }
    }
  2. View Allure reports

    master

    CUTE integrates with Allure for detailed reporting. To view reports:

    1. Install the Allure framework (e.g., brew install allure).
    2. Run your tests to generate results.
    3. Serve the results directory using the allure serve command.
    # Install Allure
    brew install allure
    
    # Serve results
    allure serve ./examples/allure-results
  3. Implement table tests in two ways

    master

    You can execute a collection of tests using two different patterns. Both patterns produce identical Allure reports.

    1. Builder table tests: Use cute.NewTestBuilder() to configure metadata (Title, Tag, Description), call CreateTableTest(), provide tests via PutTests(...), and execute them with ExecuteTest(ctx, t).
    2. Array tests: Manually iterate over a slice of *cute.Test and call test.Execute(ctx, t) on each element.

    Both methods allow you to define a slice of *cute.Test objects containing Name, Middleware, Request (with Builders), and Expect configurations.

    // Builder pattern
    cute.NewTestBuilder().
        Title("Example table test").
        Tag("table_test").
        Description("Execute array tests").
        CreateTableTest().
        PutTests(tests...).
        ExecuteTest(context.Background(), t)
    
    // Array pattern
    for _, test := range tests {
        test.Execute(context.Background(), t)
    }
  4. Organize tests using Suites

    master

    For large test collections, use suite.Suite (from allure-go) to organize tests.

    1. Define a struct that embeds suite.Suite and includes a *cute.HTTPTestMaker.
    2. Implement BeforeAll(t provider.T) to initialize the testMaker and common resources (like a base URL).
    3. Define test methods on the struct using provider.T.
    4. Run the suite using suite.RunSuite(t, new(YourSuiteStruct)).
    import (
        "github.com/ozontech/cute"
        "github.com/ozontech/allure-go/pkg/framework/provider"
        "github.com/ozontech/allure-go/pkg/framework/suite"
        "net/url"
    )
    
    type ExampleSuite struct {
        suite.Suite
        host *url.URL
        testMaker *cute.HTTPTestMaker
    }
    
    func (i *ExampleSuite) BeforeAll(t provider.T) {
        i.testMaker = cute.NewHTTPTestMaker()
        host, _ := url.Parse("https://jsonplaceholder.typicode.com/")
        i.host = host
    }
    
    func (i *ExampleSuite) TestExample_OneStep(t provider.T) {
        testBuilder := i.testMaker.NewTestBuilder()
        testBuilder.Title("One Step").Create()...
    }
    
    func TestExampleTest(t *testing.T) {
        suite.RunSuite(t, new(ExampleSuite))
    }
  5. How ExecuteTest handles different provider types

    master

    The ExecuteTest method behaves differently depending on the type of tProvider passed to it:

    1. provider.StepCtx: If the provider is a step context, cute executes all registered tests inside that specific step using executeTestsInsideStep. This is useful for grouping multiple assertions or sub-tests under a single Allure step.
    2. *testing.T: If a standard Go testing object is passed, cute creates a new Allure-wrapped test context using createAllureT. It then executes the tests normally.
    3. provider.T: If an existing Allure provider is passed, cute uses it directly to execute the tests.

    If t is nil, the method will panic.

  6. Integrate Allure reporting with CUTE

    master

    CUTE supports Allure report integration by mapping internal configuration fields to an allureProvider. The integration automatically synchronizes labels, links, and information (title, description, stage) from the CUTE instance to the Allure provider.

    To use this, the allureProvider passed to the internal setAllureInformation method must implement the following interfaces (which are typically satisfied by the Allure Go SDK):

    // The integration relies on these provider interfaces:
    // labelsAllureProvider, linksAllureProvider, and infoAllureProvider
    // which map to Allure's reporting capabilities.
  7. Configure Allure environment variables

    master

    CUTE uses Allure for reporting. You can configure the output behavior using the following global environment variables:

    KeyMeaningDefault
    ALLURE_OUTPUT_PATHPath to output allure results. (Folder with tests)
    ALLURE_OUTPUT_FOLDERName of result folder/allure-results
    ALLURE_ISSUE_PATTERNURL pattern to issue. Must contain %s
    ALLURE_TESTCASE_PATTERNURL pattern to TestCase. Must contain %s
    ALLURE_LAUNCH_TAGSDefault tags for all tests (comma-separated)
  8. Implement a single-step HTTP test

    master

    Use cute.NewTestBuilder() to create a single-request test. You can define the title, description, request parameters (URI, method, headers), and expectations (status code, timeouts, body assertions) before executing the test with ExecuteTest.

    import (
        "context"
        "net/http"
        "testing"
        "time"
    
        "github.com/ozontech/cute"
        "github.com/ozontech/cute/asserts/json"
    )
    
    func TestExample(t *testing.T) {
        cute.NewTestBuilder().
            Title("Title").
            Description("some_description").
            Create().
            RequestBuilder(
                cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"),
                cute.WithMethod(http.MethodGet),
            ).
            ExpectExecuteTimeout(10*time.Second).
            ExpectStatus(http.StatusOK).
            AssertBody(
                json.Equal("$[0].email", "Eliseo@gardner.biz"),
                json.Present("$[1].name"),
            ).
            ExecuteTest(context.Background(), t)
    }
  9. Implement a multi-step HTTP test

    master

    You can chain multiple requests within a single test by using NextTest(). To pass data between steps (e.g., capturing a status code or body from the first request to use in the second), use AfterTestExecute.

    import (
        "context"
        "fmt"
        "net/http"
        "testing"
    
        "github.com/ozontech/cute"
    )
        
    func Test_TwoSteps(t *testing.T) {
        responseCode := 0
    
        // First step
        cute.NewTestBuilder().
            Title("Test with two requests and parse body.").
            Tag("two_steps").
            Create().
            RequestBuilder(
                cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"),
                cute.WithMethod(http.MethodGet),
            ).
            ExpectStatus(http.StatusOK).
            NextTest().
    
            // Execute after first step and parse response code
            AfterTestExecute(func(response *http.Response, errors []error) error { 
                responseCode = response.StatusCode
    
                return nil
            }).
    
            // Second step
            Create().
            RequestBuilder(
                cute.WithURI("https://jsonplaceholder.typicode.com/posts/2/comments"),
                cute.WithMethod(http.MethodDelete),
            ).
            ExecuteTest(context.Background(), t)
    
        fmt.Println("Response code from first request", responseCode)
    }
  10. Validate JSON schemas

    master

    You can validate a JSON schema using one of three methods depending on where your schema is located:

    • ExpectJSONSchemaString(string): Validates from a raw string.
    • ExpectJSONSchemaByte([]byte): Validates from a byte array.
    • ExpectJSONSchemaFile(string): Validates from a file or a remote resource.
  11. Use ready-made Header asserts

    master

    The cute/asserts/headers package provides functions to validate HTTP headers:

    • Present(headerName): Asserts that the specified header is present.
    • NotPresent(headerName): Asserts that the specified header is not present.