-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds DevMock Service #7
base: main
Are you sure you want to change the base?
Changes from 6 commits
d9c674f
6678478
c760a1b
b60b743
3033cd0
fb8dc30
fd9b7f0
2af0a5f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
## Dev Mock Service | ||
|
||
Dev Mock Service Worker is an API mocking library that allows you to specify | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't actually a server worker is it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whoops, bad copy paste 😅 |
||
custom responses for any API calls inside your app. | ||
|
||
### Capabilities | ||
|
||
- Mock API requests | ||
- Dev/Prod switch | ||
|
||
### Installation | ||
|
||
Add the line `import { DevMockMode, DevMock } from '@devvit/kit';` in the beginning of your root component. | ||
|
||
### API mocks | ||
|
||
With custom handler functions, you can override the response for any API call, such as | ||
Redis, RedditAPI, or HTTP request. | ||
|
||
- In Dev mode (`DevMockMode.Dev`), handlers are applied for all requests with the matching method and ID (if available). | ||
- In Prod mode (`DevMockMode.Prod`), handlers are ignored. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will encourage users to ship development-only code which may be large. I'd love to see this code fall out of apps for at least non-prerelease builds so there wasn't a wrong way to use it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How can I achieve this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like the introduction of a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we can implement the IS_PLAYTEST and tree shaking separately, so it does not block this PR |
||
|
||
#### Setup | ||
|
||
Create devv versions of the API clients you want to mock. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this in the test file or in the real app code? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Real app code, this molecule is not intended to be used in unit tests (however it certainly can be) |
||
|
||
```typescript | ||
const { devvRedis, devvRedditApi, devvFetch } = DevMock.createService({ | ||
context, | ||
mode: DevMockMode.Dev, | ||
handlers: [ | ||
DevMock.redis.get("mocked_key", () => "Value from mocks!"), | ||
DevMock.fetch.get("https://example.com", () => | ||
DevMock.httpResponse.ok({ fetched: "mock" }), | ||
), | ||
DevMock.reddit.getSubredditById((id: string) => ({ name: `mock_${id}` })), | ||
], | ||
}); | ||
``` | ||
|
||
Use devv versions of API clients in your app. | ||
|
||
```typescript | ||
const redisValue = await devvRedis.get("mocked_key"); // "Value from mocks!" | ||
const fetchedValue = await(await devvFetch("https://example.com")).json(); // {fetched: "mock"} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: double await sort of always been a weird pattern to me instead of an intermediate var. Especially to noobies There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just an example to demonstrate the responses |
||
const redditApiValue = (await devvRedditApi.getSubredditById("t5_123")).name; // "mock_t5_123" | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
import { DevMock, DevMockMode } from "./dev-mock-service.js"; | ||
import type { Devvit } from "@devvit/public-api"; | ||
import type { Mock } from "vitest"; | ||
import type { Subreddit } from "@devvit/public-api"; | ||
|
||
describe("dev mock service", () => { | ||
const mockContext: Devvit.Context = { | ||
useState: vi.fn(), | ||
redis: { | ||
get: vi.fn(), | ||
}, | ||
reddit: { | ||
getSubredditById: vi.fn(), | ||
getCurrentUser: vi.fn(), | ||
}, | ||
} as unknown as Devvit.Context; | ||
|
||
afterEach(() => { | ||
(mockContext.useState as Mock).mockReset(); | ||
(mockContext.redis.get as Mock).mockReset(); | ||
(mockContext.reddit.getSubredditById as Mock).mockReset(); | ||
(mockContext.reddit.getCurrentUser as Mock).mockReset(); | ||
}); | ||
|
||
describe("init api", () => { | ||
describe("redis", () => { | ||
it("returns devvRedis", () => { | ||
const devMockService = DevMock.createService({ | ||
context: mockContext, | ||
mode: DevMockMode.Prod, | ||
}); | ||
expect(devMockService.devvRedis).toBeDefined(); | ||
}); | ||
|
||
it("returns devvRedis that calls the original method if no handlers are provided", async () => { | ||
(mockContext.redis.get as Mock).mockResolvedValue("real redis"); | ||
const devMockService = DevMock.createService({ | ||
context: mockContext, | ||
mode: DevMockMode.Dev, | ||
}); | ||
const response = await devMockService.devvRedis.get("regular_key"); | ||
expect(mockContext.redis.get).toBeCalledWith("regular_key"); | ||
expect(mockContext.redis.get).toHaveBeenCalledOnce(); | ||
expect(response).toBe("real redis"); | ||
}); | ||
|
||
it("returns devvRedis that applies mock responses", async () => { | ||
(mockContext.redis.get as Mock).mockResolvedValue("real redis"); | ||
const devMockService = DevMock.createService({ | ||
context: mockContext, | ||
mode: DevMockMode.Dev, | ||
handlers: [DevMock.redis.get("mocked_key", () => "mocked_response")], | ||
}); | ||
const response = await devMockService.devvRedis.get("mocked_key"); | ||
expect(mockContext.redis.get).not.toBeCalled(); | ||
expect(response).toBe("mocked_response"); | ||
}); | ||
|
||
it("ignores mocks in prod mode", async () => { | ||
(mockContext.redis.get as Mock).mockResolvedValue("real redis"); | ||
const devMockService = DevMock.createService({ | ||
context: mockContext, | ||
mode: DevMockMode.Prod, | ||
handlers: [DevMock.redis.get("mocked_key", () => "mocked_response")], | ||
}); | ||
const response = await devMockService.devvRedis.get("mocked_key"); | ||
expect(mockContext.redis.get).toBeCalledWith("mocked_key"); | ||
expect(mockContext.redis.get).toHaveBeenCalledOnce(); | ||
expect(response).toBe("real redis"); | ||
}); | ||
}); | ||
describe("redditApi", () => { | ||
it("returns devvRedditApi", () => { | ||
const devMockService = DevMock.createService({ | ||
context: mockContext, | ||
mode: DevMockMode.Prod, | ||
}); | ||
expect(devMockService.devvRedditApi).toBeDefined(); | ||
}); | ||
|
||
it("calls the original method if no handlers are provided", async () => { | ||
(mockContext.reddit.getCurrentUser as Mock).mockResolvedValue({ | ||
name: "real_user", | ||
}); | ||
const devMockService = DevMock.createService({ | ||
context: mockContext, | ||
mode: DevMockMode.Dev, | ||
}); | ||
const response = await devMockService.devvRedditApi.getCurrentUser(); | ||
expect(mockContext.reddit.getCurrentUser).toHaveBeenCalledOnce(); | ||
expect(response).toStrictEqual({ name: "real_user" }); | ||
}); | ||
|
||
it("calls the mock handler if provided", async () => { | ||
(mockContext.reddit.getSubredditById as Mock).mockResolvedValue({ | ||
name: "realSubreddit", | ||
}); | ||
const devMockService = DevMock.createService({ | ||
context: mockContext, | ||
mode: DevMockMode.Dev, | ||
handlers: [ | ||
DevMock.reddit.getSubredditById( | ||
(id: string) => ({ name: `mock_${id}` }) as Subreddit, | ||
), | ||
], | ||
}); | ||
const response = | ||
await devMockService.devvRedditApi.getSubredditById("test"); | ||
expect(mockContext.reddit.getSubredditById).not.toBeCalled(); | ||
expect(response).toStrictEqual({ name: "mock_test" }); | ||
}); | ||
|
||
it("ignores mocks in prod mode", async () => { | ||
(mockContext.reddit.getSubredditById as Mock).mockResolvedValue({ | ||
name: "realSubreddit", | ||
}); | ||
const devMockService = DevMock.createService({ | ||
context: mockContext, | ||
mode: DevMockMode.Prod, | ||
handlers: [ | ||
DevMock.reddit.getSubredditById( | ||
(id: string) => ({ name: `mock_${id}` }) as Subreddit, | ||
), | ||
], | ||
}); | ||
const response = | ||
await devMockService.devvRedditApi.getSubredditById("test"); | ||
expect(mockContext.reddit.getSubredditById).toBeCalled(); | ||
expect(response).toStrictEqual({ name: "realSubreddit" }); | ||
}); | ||
}); | ||
|
||
describe("httpApi", () => { | ||
const mockFetch = vi.fn(); | ||
const originalFetch = global.fetch; | ||
|
||
beforeEach(() => { | ||
mockFetch.mockReset(); | ||
global.fetch = mockFetch; | ||
}); | ||
|
||
afterEach(() => { | ||
global.fetch = originalFetch; | ||
}); | ||
|
||
it("returns devvFetch", () => { | ||
const devMockService = DevMock.createService({ | ||
context: mockContext, | ||
mode: DevMockMode.Prod, | ||
}); | ||
expect(devMockService.devvFetch).toBeDefined(); | ||
}); | ||
|
||
it("calls the original method if no handlers are provided", async () => { | ||
mockFetch.mockResolvedValue({ | ||
json: () => Promise.resolve({ real: "data" }), | ||
}); | ||
const devMockService = DevMock.createService({ | ||
context: mockContext, | ||
mode: DevMockMode.Dev, | ||
}); | ||
const response = await devMockService.devvFetch( | ||
"https://example.com", | ||
{}, | ||
); | ||
expect(mockFetch).toHaveBeenCalledOnce(); | ||
expect(mockFetch).toBeCalledWith("https://example.com", {}); | ||
expect(await response.json()).toStrictEqual({ real: "data" }); | ||
}); | ||
|
||
it("uses handler if provided", async () => { | ||
const devMockService = DevMock.createService({ | ||
context: mockContext, | ||
mode: DevMockMode.Dev, | ||
handlers: [ | ||
DevMock.fetch.get("https://example.com", () => { | ||
return DevMock.httpResponse.ok({ mocked: "response" }); | ||
}), | ||
], | ||
}); | ||
const response = await devMockService.devvFetch("https://example.com", { | ||
method: "GET", | ||
}); | ||
expect(mockFetch).not.toBeCalled(); | ||
expect(await response.json()).toStrictEqual({ mocked: "response" }); | ||
}); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import type { RedditAPIClient, RedisClient } from "@devvit/public-api"; | ||
import type { Devvit } from "@devvit/public-api"; | ||
import type { HandlerOverride } from "./types/index.js"; | ||
|
||
import { | ||
createDevvRedis, | ||
isRedisOverride, | ||
redisHandler, | ||
} from "./redis-mock-service/index.js"; | ||
import { | ||
createDevvRedditApi, | ||
isRedditApiOverride, | ||
redditApiHandler, | ||
} from "./reddit-api-mock-service/index.js"; | ||
import { | ||
createDevvFetch, | ||
httpHandler, | ||
httpResponse, | ||
isHttpApiOverride, | ||
} from "./http-mock-service/index.js"; | ||
|
||
export enum DevMockMode { | ||
Prod = "Prod", | ||
Dev = "Dev", | ||
} | ||
|
||
export type DevMockService = { | ||
devvRedis: RedisClient; | ||
devvRedditApi: RedditAPIClient; | ||
devvFetch: typeof fetch; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggest omitting the name devv prefix names (I make a comment about this later that it's a new term) because I don't think it's adding much useful context here. the user only needs to know which service is which and the typing covers that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is done to avoid confusion with the actual instances of const {redis, reddit} = context;
const { devvRedis, devvRedditApi, devvFetch } = DevMock.createService({... Without the prefix there is a potential name collision |
||
}; | ||
|
||
const createDevMockService = (config: { | ||
context: Devvit.Context; | ||
mode: DevMockMode; | ||
handlers?: HandlerOverride[]; | ||
}): DevMockService => { | ||
if (config.mode === DevMockMode.Prod) { | ||
return { | ||
devvRedis: config.context.redis, | ||
devvRedditApi: config.context.reddit, | ||
devvFetch: fetch, | ||
}; | ||
} | ||
const redisHandlers = config.handlers?.filter(isRedisOverride) || []; | ||
const devvRedis = createDevvRedis(config.context.redis, redisHandlers); | ||
|
||
const redditApiHandlers = config.handlers?.filter(isRedditApiOverride) || []; | ||
const devvRedditApi = createDevvRedditApi( | ||
config.context.reddit, | ||
redditApiHandlers, | ||
); | ||
|
||
const httpApiHandlers = config.handlers?.filter(isHttpApiOverride) || []; | ||
const devvFetch = createDevvFetch(fetch, httpApiHandlers); | ||
|
||
return { devvRedis, devvRedditApi, devvFetch }; | ||
}; | ||
|
||
export const DevMock = { | ||
createService: createDevMockService, | ||
redis: redisHandler, | ||
reddit: redditApiHandler, | ||
fetch: httpHandler, | ||
httpResponse: httpResponse, | ||
} as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔕 suggest consider renaming to just "mocks". "dev" and "service" aren't buying me much. same thing with the subdirectory names which could be like mock-reddit instead of reddit-api-mock-service.