DEV Community

keploy
keploy

Posted on • Originally published at keploy.io

Stubbing and Verifying: My Journey to Smarter Testing 🚀

Image description
Let’s talk about stubbing and verifying - two critical tools that transformed how I approach testing. Early in my career, testing was often frustrating, slow, and full of surprises, most of them bad. I used to think testing meant running my app against real systems and crossing my fingers that everything worked. Spoiler: it rarely did.

Here’s the thing: testing doesn’t have to be painful. Let me share how embracing stubbing and verifying - and tools like Keploy - helped me with testing.

The Problem: Testing Dependencies is Messy

Picture this: you're testing a piece of code that relies on an external API, a database, or some other dependency. Ideally, your test should just focus on your code. But when you call that dependency:

  • The API goes down.

  • The database has missing or outdated data.

  • Network latency makes tests drag forever.

  • Oh, and let’s not even talk about hitting rate limits on a third-party API.

Testing in this way is like trying to build a sandcastle during high tide - frustrating and doomed to fail. Relying on real systems for testing is just not practical.

The First Breakthrough: Stubbing 🛠️

Here’s where stubbing comes in. Instead of depending on real systems, I started creating controlled responses - fake data, if you will. A stub is like an actor playing a role: it gives a predictable performance no matter what chaos is happening offstage.

A Simple Analogy

Say you’re testing a food delivery app. Instead of calling the real kitchen to check the pizza status, you create a stub that always responds, “Your pizza is on the way!” That’s stubbing in a nutshell: controlling the outcome to test your app without relying on real-world systems.

In code, stubbing often looks something like this:

// Define a struct for the response
type Response struct {
    Status  int    `json:"status"`
    Message string `json:"message"`
}

// Create a mock function that returns the response
fakeAPI := func() func() (*Response, error) {
    return func() (*Response, error) {
        return &Response{
            Status:  200,
            Message: "Pizza is on the way!",
        }, nil
    }
}()
Enter fullscreen mode Exit fullscreen mode

This approach keeps tests fast, reliable, and independent of external factors.

Meet Keploy: Smarter Stubbing at Scale

While manual stubs work for small tests, scaling them across a complex application can be tricky. That’s where Keploy comes in. It’s a powerful open-source tool that simplifies creating data stubs. What makes Keploy stand out is its ability to automatically record real-world interactions (e.g., API calls or database queries) and convert them into reusable test cases or stubs.

How Keploy Works

  1. Capture Data Interactions: Run your app once with Keploy enabled, and it records all external interactions.

  2. Generate Stubs: Keploy converts these real interactions into test cases or mock data.

  3. Replay and Verify: Use the generated stubs in your tests, ensuring predictable and consistent responses.

This eliminates the need to write stubs manually, saving time and reducing errors.

Adding Confidence: Verification 🕵️‍♂️

While stubbing ensures your code doesn’t depend on flaky systems, verification ensures your code interacts with those systems correctly. This is where you check things like:

  • Did your app call the function it was supposed to?

  • Was it called with the right arguments?

  • How many times was it called?

For example:

func TestFakeAPIWithTestify(t *testing.T) {
    assert := assert.New(t)
    mock := new(MockAPI)
    mock.On("Call", "extra cheese").Return(&Response{
        Status:  200,
        Message: "Pizza is on the way!",
    }, nil)

    // Call the API
    mock.Call("extra cheese")

    mock.AssertCalled(t, "Call", "extra cheese")
    mock.AssertNumberOfCalls(t, "Call", 1)
}
Enter fullscreen mode Exit fullscreen mode

Verification catches those subtle bugs that can sneak in when your app doesn’t behave as expected. It’s the difference between assuming your app works and proving it does.

A Practical Example: Testing a Weather App

Let’s say you’re building a weather app that fetches data from an external API. With mocking/stubbing, your test might look something like this:

package main

import (
    "encoding/json"
    "io"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"

    "github.com/stretchr/testify/assert"
)

// mockWeatherResponse creates a mock weather response
func mockWeatherResponse() WeatherResponse {
    return WeatherResponse{
        Latitude:  52.52,
        Longitude: 13.41,
        Timezone:  "America/Denver",
        Current: struct {
            Temperature2m float64 `json:"temperature_2m"`
        }{
            Temperature2m: 20.5,
        },
        Hourly: struct {
            Time          []string  `json:"time"`
            Temperature2m []float64 `json:"temperature_2m"`
        }{
            Time:          []string{"2025-01-07T00:00", "2025-01-07T01:00"},
            Temperature2m: []float64{20.5, 19.8},
        },
        Daily: struct {
            Time        []string `json:"time"`
            WeatherCode []int    `json:"weather_code"`
            Sunrise     []string `json:"sunrise"`
            Sunset      []string `json:"sunset"`
        }{
            Time:        []string{"2025-01-07"},
            WeatherCode: []int{0},
            Sunrise:     []string{"2025-01-07T07:15"},
            Sunset:      []string{"2025-01-07T16:45"},
        },
    }
}

// MockOpenMeteoServer creates a test server that mimics the OpenMeteo API
func MockOpenMeteoServer() *httptest.Server {
    return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        mockResponse := mockWeatherResponse()
        json.NewEncoder(w).Encode(mockResponse)
    }))
}

func TestAPIEndpoint(t *testing.T) {
    assert := assert.New(t)

    // Create a request to our API endpoint
    req := httptest.NewRequest("GET", "/api/weather", nil)
    w := httptest.NewRecorder()

    // Call the handler
    weatherHandler(w, req)

    // Check response
    resp := w.Result()
    assert.Equal(http.StatusOK, resp.StatusCode)
    assert.Equal("application/json", resp.Header.Get("Content-Type"))

    // Parse response body
    var weather WeatherResponse
    err := json.NewDecoder(resp.Body).Decode(&weather)
    assert.NoError(err)

    // Verify response data
    assert.Equal(52.52, weather.Latitude)
    assert.Equal(13.419998, weather.Longitude)
    assert.Equal("America/Denver", weather.Timezone)
    assert.Equal(6.3, weather.Current.Temperature2m)
}

func TestHTMLEndpoint(t *testing.T) {
    assert := assert.New(t)

    // Create a request to our HTML endpoint
    req := httptest.NewRequest("GET", "/weather", nil)
    w := httptest.NewRecorder()

    // Call the handler
    weatherHTMLHandler(w, req)

    // Check response
    resp := w.Result()
    assert.Equal(http.StatusOK, resp.StatusCode)
    assert.Equal("text/html", resp.Header.Get("Content-Type"))

    // Read response body
    body, err := io.ReadAll(resp.Body)
    assert.NoError(err)

    // Check if HTML contains expected elements
    htmlContent := string(body)
    assert.True(strings.Contains(htmlContent, "<title>Weather Forecast</title>"))
    assert.True(strings.Contains(htmlContent, "Current Weather"))
    assert.True(strings.Contains(htmlContent, "Daily Forecast"))
    assert.True(strings.Contains(htmlContent, "Hourly Forecast"))
}

func TestGetWeatherDescription(t *testing.T) {
    assert := assert.New(t)

    testCases := []struct {
        code     int
        expected string
    }{
        {0, "Clear sky"},
        {1, "Mainly clear"},
        {95, "Thunderstorm"},
        {999, "Unknown weather condition"},
    }

    for _, tc := range testCases {
        result := getWeatherDescription(tc.code)
        assert.Equal(tc.expected, result)
    }
}
Enter fullscreen mode Exit fullscreen mode

Looks fine, right? But what happens if:

  • The API is down?

  • The API response format changes?

  • The network is slow or flaky?

Instead, with Keploy, you can capture the API response once and reuse it:

Keploy - Generated Testcases

Keploy handles the heavy lifting, giving you a reliable and scalable way to manage test data without manually creating or maintaining stubs. This is show our mocks would look like: -

Keploy - Data Mocks

The Payoff: Faster, Better Testing

Once I embraced stubbing and verifying (with a help from Keploy), everything changed:

  1. Speed: Tests ran in seconds instead of minutes.

  2. Reliability: Tests didn’t fail because of flaky external systems.

  3. Clarity: When something broke, I knew it was my code - not the system I was testing against.

Pro Tips for Effective Stubbing and Verifying

Now that you see the benefits, let’s talk about how to use stubs and verifications effectively:

  1. Don’t Overdo It: Stub only what you need. If you fake too much, your tests can lose their connection to real-world behavior.

  2. Reset Between Tests: Always clean up stubs and mocks after each test to avoid contamination. Tools like Keploy handle this automatically.

  3. Name Clearly: Use descriptive names for your stubs and mocks so other developers (or your future self) understand what’s happening.

  4. Combine with Mocks: While stubs fake responses, mocks can check behavior (like how many times a function is called). Use both together for robust testing.

  5. Leverage Automation: Tools like Keploy streamline the creation and management of stubs, so you can focus on writing meaningful tests.

Why It Matters

Testing is all about confidence - confidence that your code works, that it handles edge cases, and that changes won’t break anything important. Stubbing and verifying gave me that confidence. Tools like Keploy took it a step further, making testing faster, easier, and more scalable.

These tools aren’t just for big projects or senior devs. Whether you’re working on a hobby project or a production app, stubbing and verifying (with automation) can save you time, frustration, and headaches.

Conclusion

Testing with stubbing and verifying isn’t about cutting corners—it’s about being smart. It lets you focus on your code and isolate problems without worrying about the outside world. Once you get the hang of it, you’ll wonder how you ever managed without it.

So, next time you’re testing something, remember:

  • Stub the response to control the test environment.

  • Verify the behavior to ensure everything works as it should.

  • Automate the tedious parts with tools like Keploy.

Testing doesn’t have to be frustrating. With the right tools, it can even be (dare I say) fun. 😊

FAQs

What’s the difference between stubbing and mocking?

  • Stubbing: It focuses on controlling the output of dependencies. For example, you provide fake data or responses (e.g., “Pizza is on the way!”) without verifying how the dependency was used.

  • Mocking: It involves both stubbing and verification. Mocks not only return controlled responses but also track interactions, like the number of times a function was called and the arguments passed.

How does Keploy differ from traditional stubbing tools?

Keploy automates the creation of stubs by capturing real-world interactions (e.g., API calls or database queries) and converting them into reusable test cases. Unlike manual stubbing:

  • It reduces human effort in creating and maintaining mocks.

  • Ensures consistency by capturing live data.

  • Makes scaling stubs across large applications more manageable.

When should I use stubbing vs. real-world testing?

  • Stubbing: Use it during unit and integration tests where speed, reliability, and control are priorities. It’s ideal for testing isolated components without relying on external systems.

  • Real-world testing: Use it for end-to-end tests to ensure the entire system (including dependencies) works as expected. However, these tests are typically slower and less reliable due to external factors.

What’s the best way to combine stubbing and verifying in tests?

To achieve robust testing:

  1. Stub external dependencies to isolate your code and ensure predictable results.

  2. Verify interactions to confirm your code behaves correctly, such as calling the right functions with expected arguments.

  3. Use tools like Keploy to automate stubbing and clean up mocks between tests to avoid interference.

Top comments (0)