
# The Problem

I once worked with a developer, let's call him John, who to test new features would walk laps around the office with his phone so it would sync real Fitbit steps. He'd pace, check his phone, and even use lunch breaks to gather more data. Accurate? Yes. Practical for development? Absolutely not.

In the meantime I could not do anything related to this feature because, well... I did not have a device. And there was no easy hack for me to make it work in development.

You can probably also imagine using production credentials by accident or hell, even on purpose. Perhaps, the service you are integrating with doesn't allow you to have more than 1 API key.

To give you an idea of what our code looked like:

```ts
class StepTracker {
  private readonly _fitbitApi: FitbitApi;
}
```

The problem here is that `StepTracker` directly depends on the FitbitApi, which makes it hard for us to "replace" it. ~~we are using TS so we could technically cheat our way!~~

<Mermaid
  chart={`
    graph LR
        StepTracker[StepTracker] -->|depends on| FitbitApi[FitbitApi<br/>Needs Auth & Device]
  `}
/>

## Introducing the interface

> An interface in this context is also referred to as a contract[^contract].

A simple way to get rid of this coupling is to introduce an interface between the core service and the external dependency. This interface should be framework-agnostic; it should describe what your app needs, not mirror the Fitbit SDK:

```ts
type Result<T> =
  | { success: true; value: T }
  | { success: false; error: string };

interface IStepCounter {
  getStepsToday(): Promise<Result<number>>;
  resetDailyCount(): Promise<Result<void>>;
}

class StepTracker {
  private readonly _stepCounter: IStepCounter;
}

class FitbitStepCounter implements IStepCounter {
  /** fitbit implementation **/
}
```

With this change we no longer depend on the concrete implementation of `Fitbit` but instead we depend on our own defined interface. You can see the relation of the dependencies
more clearly in the diagram below; notice the arrows.

<Mermaid
  chart={`
    graph LR
        StepTracker[StepTracker] -->|depends on| IStepCounter[IStepCounter]
        FitbitStepCounter[FitbitStepCounter] -->|implements| IStepCounter
  `}
/>

The benefit of this is that we can easily create multiple implementations, like this `FakeStepCounter` for example.

```ts
class FakeStepCounter implements IStepCounter {
  private _steps: number = 0;

  public async getStepsToday(): Promise<Result<number>> {
    return { success: true, value: this._steps };
  }

  public async resetDailyCount(): Promise<Result<void>> {
    this._steps = 0;
    return { success: true, value: undefined };
  }
}
```

This is the ideal form of plugability, we can always swap to different step tracking services without having to change our own logic.

<Mermaid
  chart={`
    graph LR
        StepTracker[StepTracker] -->|depends on| IStepCounter[IStepCounter]
        subgraph Implementations
          FakeStepCounter[FakeStepCounter<br/>No Hardware]
          FitbitStepCounter[FitbitStepCounter<br/>Production]
        end
        Implementations -->|implements| IStepCounter
  `}
/>

Now I don't have to depend on John anymore and instead, I can just use the `FakeStepCounter`, wonderful.

## Do they behave the same way?

They both appear to return steps, and with a little bit of customization we could insert steps into our `FakeStepCounter` during development. However,
if we look closely at the contract, we can see that it always returns a `Result` type, but we are never testing for the failed branch. To tackle this we can do 2 things:

- Improve our `fake` implementation to be closer to the real deal and/or allow us to simulate states.
- Go the contract testing route where we simply make sure that every implementation behaves the same way. ~~excluding exceptions~~

We will do the latter, because, frankly, this gives us enough confidence that it works and during development I don't want to see it fail, generally.

## Contract Test (FakeStepCounter)

```ts
describe("Contract tests", () => {
  test("should return current step count on success", async () => {
    const stepCounter = new FakeStepCounter(5000, true);

    const result = await stepCounter.getStepsToday();

    expect(result.success).toBe(true);
    expect(result.value).toBe(5000);
  });

  test("should return error on failure", async () => {
    const stepCounter = new FakeStepCounter(0, false);

    const result = await stepCounter.getStepsToday();

    expect(result.success).toBe(false);
    expect(result.error).toBe("Failed to fetch steps");
  });

  test("should reset daily count to zero on success", async () => {
    const stepCounter = new FakeStepCounter(10000, true);
    await stepCounter.resetDailyCount();

    const result = await stepCounter.getStepsToday();

    expect(result.success).toBe(true);
    expect(result.value).toBe(0);
  });
});
```

To support this, we modify the fake to be controllable:

```ts
class FakeStepCounter implements IStepCounter {
  constructor(
    private _steps: number = 0,
    private _shouldSucceed: boolean = true
  ) {}

  async getStepsToday(): Promise<Result<number>> {
    if (!this._shouldSucceed) {
      return { success: false, error: "Failed to fetch steps" };
    }
    return { success: true, value: this._steps };
  }

  async resetDailyCount(): Promise<Result<void>> {
    if (!this._shouldSucceed) {
      return { success: false, error: "Failed to reset steps" };
    }
    this._steps = 0;
    return { success: true, value: undefined };
  }
}
```

Now we can simulate both success and error paths.

## What about the real Fitbit implementation?

To test `FitbitStepCounter`, we inject a stubbed[^stub] SDK. This isolates our adapter[^adapter] logic, verifying that we correctly translate SDK responses to our Result contract without hitting real servers. For the actual API connection, rely on a staging smoke test or my favorite: see it work once and move on.

## Final Contract Test

To ensure both our fake and our real adapter follow the same rules, we write a reusable test suite.[^test-suite] With a small refactoring, we get the following:

```ts
type Factory<T> = (initialSteps: number, shouldSucceed: boolean) => T;

function testStepCounterContract(
  name: string,
  factory: Factory<IStepCounter>
): void {
  describe(`${name}`, () => {
    test("should return current step count on success", async () => {
      const stepCounter = factory(5000, true);

      const result = await stepCounter.getStepsToday();

      expect(result.success).toBe(true);
      expect(result.value).toBe(5000);
    });

    test("should return error on failure", async () => {
      const stepCounter = factory(0, false);

      const result = await stepCounter.getStepsToday();

      expect(result.success).toBe(false);
      expect(result.error).toBeDefined();
    });

    test("should reset daily count to zero on success", async () => {
      const stepCounter = factory(10000, true);
      const resetResult = await stepCounter.resetDailyCount();

      expect(resetResult.success).toBe(true);

      const result = await stepCounter.getStepsToday();

      expect(result.success).toBe(true);
      expect(result.value).toBe(0);
    });
  });
}

describe("IStepCounter Contract", () => {
  testStepCounterContract(
    "FakeStepCounter",
    (initialSteps, shouldSucceed) =>
      new FakeStepCounter(initialSteps, shouldSucceed)
  );

  testStepCounterContract(
    "FitbitStepCounter",
    (initialSteps, shouldSucceed) => {
      return new FitbitStepCounter(stub(initialSteps, shouldSucceed));
    }
  );
});
```

When you run this, you'll see output like:

```
PASS  tests/StepCounterContractTests.ts
  IStepCounter Contract
    FakeStepCounter
      ✓ should return current step count on success
      ✓ should return error on failure
      ✓ should reset daily count to zero on success
    FitbitStepCounter
      ✓ should return current step count on success
      ✓ should return error on failure
      ✓ should reset daily count to zero on success

Test Files  1 passed (1)
     Tests  6 passed (6)
  Start at  14:23:45
  Duration  245ms
```

At this point, you've proven that both implementations follow the contract. Whether the actual Fitbit API works is a separate concern; one tested through a form of an integration test or a manual check, not part of the contract. This also protects us from changes in the official SDK. If we only wanted to solve the real life walking problem, then a proxy (or what the cool kids call a wrapper) could also be enough.

**Takeaway**: define a stable contract, keep your core depending on it, and use contract tests to keep every adapter aligned; save integration checks for the real API.

In case **John** finally makes it back from lunch, I hope this helps you, buddy.

Thanks for reading!

[^contract]: A contract is a stable shape that others can depend on. Usually in the form of an interface.
[^adapter]: An adapter in this case is the implementation of a contract. It translates between two-sides so it works together.
[^stub]: A stub is a test double that returns an expected result.
[^test-suite]: A test suite is a collection of related tests.
