You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js Shopify应用Mocha测试报app.address is not a function错误

问题根因

TypeError: app.address is not a function报错来自两个核心问题:

  • 你导入的server根本不是chai-http可识别的HTTP服务实例:现有代码里Koa服务实例完全定义在app.prepare().then()的异步回调内部,外层没有做导出,index.js实际导出的是app.prepare()返回的Promise对象,这个对象不存在address()方法,chai-http拿到后自然报错。
  • 就算正确导出Koa实例,chai-http也需要拿到server.listen()返回的原生Node.js HTTP Server对象才能正常工作,直接传Koa实例本身也可能出现兼容问题。
修复步骤

1. 调整server.js的导出逻辑

不要把Koa初始化、路由注册的逻辑全塞在异步回调里不对外暴露,调整结构:提前初始化Koa实例和路由,等Next.js准备完成后再启动监听,同时对外暴露可访问的服务实例。
修改后的server.js核心结构如下:

import "@babel/polyfill";
import createShopifyAuth, { verifyRequest } from "@shopify/koa-shopify-auth";
import Shopify, { ApiVersion } from "@shopify/shopify-api";
import dotenv from "dotenv";
import "isomorphic-fetch";
import Koa from "koa";
import Router from "koa-router";
import next from "next";
import http from 'http';
const mongoose = require("mongoose");

dotenv.config();
const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== "production";
const nextApp = next({ dev });
const handle = nextApp.getRequestHandler();

const AccessToken = {};
const ACTIVE_SHOPIFY_SHOPS = {};

Shopify.Context.initialize({
  API_KEY: process.env.SHOPIFY_API_KEY,
  API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
  SCOPES: process.env.SCOPES.split(","),
  HOST_NAME: process.env.HOST.replace(/https:\/\/|\/$/g, ""),
  API_VERSION: ApiVersion.January22,
  IS_EMBEDDED_APP: true,
  SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
});

// 提前初始化Koa实例和路由,不要放在异步回调内
const koaServer = new Koa();
const router = new Router();
koaServer.keys = [Shopify.Context.API_SECRET_KEY];

koaServer.use(
  createShopifyAuth({
    async afterAuth(ctx) {
      const { shop, accessToken, scope } = ctx.state.shopify;
      AccessToken["access_token"] = accessToken;

      ctx.cookies.set("accessToken", accessToken, {
        httpOnly: false,
        secure: true,
        sameSite: "none",
      });

      ctx.cookies.set("shop_name", shop, {
        secure: true,
        sameSite: "none",
        httpOnly: false,
      });

      const host = ctx.query.host;
      ACTIVE_SHOPIFY_SHOPS[shop] = scope;

      ctx.cookies.set("HOST", host, {
        secure: true,
        sameSite: "none",
        httpOnly: false,
      });

      const shopName = shop.replace("myshopify.com", "");
      const formattedShopName = shopName
        .replace(/[^a-z\d\s]+/gi, "")
        .replace(/\s+/g, "")
        .toLowerCase();
      const connectionStr = `${process.env.CONNECTION_STRING}${formattedShopName}`;
      await mongoose
        .connect(connectionStr, {
          // 填写你的数据库连接配置
        })
        .then(async () => {
          console.log("database connected");
        })
        .catch((err) => console.log(err));

      ctx.redirect(`/?shop=${shop}&host=${host}`);
    },
  })
);

router.get("/customers", async (ctx, next) => {
  // 填写获取客户的业务逻辑,必须给ctx赋值返回,否则接口会挂起
  ctx.body = { success: true }
});

const handleRequest = async (ctx) => {
  await handle(ctx.req, ctx.res);
  ctx.respond = false;
  ctx.res.statusCode = 200;
};

router.get("(/_next/static/.*)", handleRequest);
router.get("/_next/webpack-hmr", handleRequest);
router.get("(.*)", verifyRequest(), handleRequest);

koaServer.use(router.allowedMethods());
koaServer.use(router.routes());

let httpServer = null;
const readyPromise = nextApp.prepare().then(() => {
  // 非测试环境正常监听端口,测试环境不占端口,交给chai-http处理
  if (process.env.NODE_ENV !== 'test') {
    httpServer = koaServer.listen(port, () => {
      console.log(`> Ready on http://localhost:${port}`);
    });
  } else {
    httpServer = http.createServer(koaServer.callback());
  }
  return httpServer;
});

// 对外导出初始化Promise和服务实例获取方法
module.exports = {
  ready: readyPromise,
  getServer: () => httpServer
}

2. 调整测试用例逻辑

Next.js的app.prepare是异步操作,必须等服务完全初始化完成后再执行测试用例,修改appTest.js:

let chai = require("chai");
let chaiHttp = require("chai-http");
const { ready, getServer } = require("../server/index");

chai.should();
chai.use(chaiHttp);


describe("GET requests", () => {
  // 测试套件执行前先等服务初始化完成
  before(async () => {
    await ready;
  });

  describe("GET /customers", () => {
    it("Success should return true", (done) => {
      chai.request(getServer())
        .get("/customers")
        .end((err, res) => {
          console.log('Error => ', err);
          console.log('Response => ', res);
          res.body.success.should.equal(true)
          done();
        });
    });
  });
});

3. 修改测试启动脚本

跑测试时需要指定NODE_ENV=test,避免服务启动时占用3000端口,修改package.json里的test脚本:

"test": "cross-env NODE_ENV=test mocha"

你项目里已经安装了cross-env,不需要额外装依赖。

额外注意事项
  • 当前/customers路由的处理函数是空的,没有任何返回值,就算服务启动成功测试也会超时挂掉,记得补上对应的响应逻辑。
  • 测试环境下verifyRequest中间件会拦截未携带Shopify认证信息的请求,直接返回302跳转,拿不到预期的JSON结果,可以加环境变量判断,测试环境下跳过这个中间件的校验。

内容的提问来源于stack exchange,提问作者sanjidulhaque

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 22:45:44