How to Simulate Fixed QPS Mock APIs with Java Semaphore and Moco

This article explains how to overcome the difficulty of isolating external service performance during load testing by creating a fixed‑QPS mock endpoint using Java's Semaphore and a custom Moco ResponseHandler, includes code samples, accuracy observations, and practical guidelines.

FunTester
FunTester
FunTester
How to Simulate Fixed QPS Mock APIs with Java Semaphore and Moco

Background and Problem

When performing interface testing, calls to other services or third‑party APIs make it hard to isolate performance variations during load testing. Maintaining a separate environment is resource‑intensive, and third‑party performance data is often unavailable.

Initial Attempt with Moco

We first tried to use the Moco API to create a fixed‑QPS mock endpoint, but after an hour of reading the documentation and trying demos, the approach proved infeasible.

Custom Solution Using Java Semaphore

We implemented a custom rate‑limiting mock by leveraging java.util.concurrent.Semaphore to control concurrency and a custom ResponseHandler that sleeps before releasing the permit. The handler receives a token, pauses for a configurable gap (in microseconds), then forwards the response.

Observed Accuracy

Tests showed error margins within 10 % and, with well‑designed test plans, typically under 5 %. Three empirical rules emerged:

More request threads reduce error.

Higher request counts reduce error.

Fully warmed‑up systems reduce error.

Demo Code

HttpServer server = getServer(8088);
server.get(urlOnly("/aaa")).response(qps(textRes("faun"), 10));
server.response("haha");
MocoServer drive = run(server);
waitForKey("fan");
drive.stop();

Utility Methods

static ResponseHandler qps(ResponseHandler handler) {
    return QPSHandler.newSeq(handler, 1000);
}
static ResponseHandler qps(ResponseHandler handler, int gap) {
    return QPSHandler.newSeq(handler, gap);
}

Implementation of QPSHandler

package com.fun.moco.support;

import com.github.dreamhead.moco.ResponseHandler;
import com.github.dreamhead.moco.handler.AbstractResponseHandler;
import com.github.dreamhead.moco.internal.SessionContext;
import com.github.dreamhead.moco.util.Idles;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import static com.google.common.base.Preconditions.checkArgument;

class QPSHandler extends AbstractResponseHandler {
    private static final Semaphore semaphore = new Semaphore(1, true);
    private final int gap;
    private final ResponseHandler handler;

    private QPSHandler(ResponseHandler handler, int gap) {
        this.gap = gap;
        this.handler = handler;
    }

    public static ResponseHandler newSeq(final ResponseHandler handler, int gap) {
        checkArgument(handler != null, "responsehandler cannot be null!");
        return new QPSHandler(handler, gap);
    }

    @Override
    void writeToResponse(SessionContext context) {
        semaphore.acquire();
        Idles.idle(gap * 1000, TimeUnit.MICROSECONDS);
        handler.writeToResponse(context);
        semaphore.release();
    }
}
JavaPerformance TestingSemaphoreRate LimitingMoCoMock API
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.