如何使用 TypeScript Partials 测试 AWS Lambda?

How can I use TypeScript Partials to test AWS Lambda?

非常相似,但我无法理解为什么部分类型被视为与完整版本不兼容。

我有一个单元测试,如果 AWS lambda 事件中的 body 无效,它会检查 lambda returns 400。为了避免给我的同事带来噪音,我不想创建具有完整 APIGatewayProxyEvent 的所有属性的 invalidEvent。因此使用 Partial<APIGatewayProxyEvent>.

  it("should return 400 when request event is invalid", async () => {
    const invalidEvent: Partial<APIGatewayProxyEvent> = {
      body: JSON.stringify({ foo: "bar" }),
    };
    const { statusCode } = await handler(invalidEvent);
    expect(statusCode).toBe(400);
  });

const { statusCode } = await handler(invalidEvent); 行编译失败:

Argument of type 'Partial<APIGatewayProxyEvent>' is not assignable to parameter of type 'APIGatewayProxyEvent'.
  Types of property 'body' are incompatible.
    Type 'string | null | undefined' is not assignable to type 'string | null'.
      Type 'undefined' is not assignable to type 'string | null'.ts(2345)

我知道 APIGatewayProxyEvent body 可以是 string | null(从类型上看)但是 string | null | undefined 是从哪里来的?为什么我的 body - 这是一个字符串 - 不是 APIGatewayProxyEvent

的有效正文

如何使用 TypeScript Partials 测试 AWS Lambda?

我可以使用 但我发现 Partials 更明确。以下代码虽然有效:

    const invalidEvent = { body: JSON.stringify({ foo: "bar" }) } as APIGatewayProxyEvent;

更新:使用 Omit 和 Pick 创建新类型

  type TestingEventWithBody = Omit<Partial<APIGatewayProxyEvent>, "body"> & Pick<APIGatewayProxyEvent, "body">;

  it("should return 400 when request event is invalid", async () => {
    const invalidEvent: TestingEventWithBody = { body: JSON.stringify({ foo: "bar" }) };
    const { statusCode } = await handler(invalidEvent);
    expect(statusCode).toBe(400);
  });

失败:

Argument of type 'TestingEventWithBody' is not assignable to parameter of type 'APIGatewayProxyEvent'.
  Types of property 'headers' are incompatible.
    Type 'APIGatewayProxyEventHeaders | undefined' is not assignable to type 'APIGatewayProxyEventHeaders'.
      Type 'undefined' is not assignable to type 'APIGatewayProxyEventHeaders'.ts(2345)

这里的问题是部分类型将所有对象属性转换为可选:

type MyObj = { myKey: string | null };

type MyPartialObj = Partial<MyObj>;
// MyPartialObj is: { myKey?: string | null | undefined }

在类型 MyObj 中,myKey 类型是 string | null。当我们将其转换为 MyPartialObj 时,myKey 类型成为可选类型,因此有可能成为 undefined。所以现在它的类型是 string | null | undefined

您的 APIGatewayProxyEvent 类型期望 bodystring | null,但是由于您将其设为部分类型,所以您说的是 body 可以也就是undefined。是的,您已经定义了它,但是您从未执行 type narrowing 来验证它确实是一个字符串。因此,所有 TypeScript 必须离开的是您分配的类型,这又是部分的。


更新: 扩展@jonrsharpe 在评论中所说的内容。我以前的解决方案似乎对以太不起作用,它只是将错误推到 APIGatewayProxyEvent 中的下一个 属性。 查看他们的回答。问题是您正在尝试模拟一部分数据,并且该类型期望所有数据都存在。制作一个每个 属性 具有最小值的对象而不是试图伪造它可能是最简单的。您 可以 使用 as 但这首先违背了使用类型系统的全部意义。


上一个答案: 一个解决方案可能是使所有值都是可选的,除了 body:

const invalidEvent: Omit<Partial<APIGatewayProxyEvent>, 'body'> & Pick<APIGatewayProxyEvent, 'body'> = {
    body: JSON.stringify({ foo: "bar" }),
};

此外,避免像瘟疫一样使用 as x,除非您完全确定自己在做什么。

I'm failing to understand why the Partial type is seen as being incompatible with the full version

从根本上说,这是不可避免的 - 您从要求 body 属性 为 string | null 的内容开始,然后创建了 string | null | undefined 要求较弱的内容。在这种情况下,您 确实 提供了 body,但这并不重要,因为 handler 只能通过 Partial<APIGatewayProxyEvent> 看到 invalidEvent接口和编译器知道 属性 可能 丢失。如您所见,如果您修补那个 属性 再次被要求,它只会抱怨下一个。

在您不拥有 handler API 的情况下,您实际上只有三个选择,none 其中最理想的是:

  1. 实际提供一个完整的APIGatewayProxyEvent(见文末的快捷方式);
  2. 向编译器声明你的测试对象一个完整的APIGatewayProxyEventtype assertion;或
  3. // @ts-ignore 注释告诉编译器根本不要检查它。

使用Partial一般只是选项2的一个步骤,使用:

const thing: Partial<Thing> = { ... };
whatever(thing as Thing);

而不是:

const thing = { ... } as Thing;
whatever(thing);

如果您 拥有 handler 的 API,最好的方法是应用 interface segregation principle 并具体说明它实际需要做什么来完成它的工作。如果只有body,例如:

type HandlerEvent = Pick<APIGatewayProxyEvent, "body">;

function handler(event: HandlerEvent) { ... } 

完整的 APIGatewayProxyEvent 仍然是 handler 的有效参数,因为它确实有一个 body(事实上它还有其他属性是无关紧要的,他们'无法通过 HandlerEvent 访问)。这也作为内置文档,说明您实际从完整对象中消耗了什么。

在您的测试中,您现在可以只创建较小的对象:

it("should return 400 when request event is invalid", async () => {
  const invalidEvent: HandlerEvent = { body: JSON.stringify({ foo: "bar" }) };
  const { statusCode } = await handler(invalidEvent);
  expect(statusCode).toBe(400);
});

作为奖励,如果后来发现您需要访问 handler 中的更多事件属性,您可以更新类型:

type HandlerEvent = Pick<APIGatewayProxyEvent, "body" | "headers">;

你会在任何需要更新测试数据以考虑到这一点的地方遇到错误。 const invalidEvent = { ... } as APIGatewayProxyEvent; 不会发生这种情况,您必须通过查看哪些测试在运行时失败来跟踪更改。


我见过的与选项 1 一起使用的另一个快捷方式是围绕部分函数包装一个函数,提供合理的默认值:

function createTestData(overrides: Partial<APIGatewayProxyEvent>): APIGatewayProxyEvent {
  return {
    body: null,
    headers: {},
    // etc.
    ...overrides,
  };
}

it("should return 400 when request event is invalid", async () => {
  const invalidEvent = createTestData({ body: JSON.stringify({ foo: "bar" }) });
  const { statusCode } = await handler(invalidEvent);
  expect(statusCode).toBe(400);
});

在这种情况下,您应该使默认值尽可能小(null0""、空对象和数组,...),以避免任何特定行为取决于他们。