第一章:大麦网Go测试金字塔重构的背景与目标
大麦网作为国内领先的票务服务平台,其核心订单系统长期采用单体架构演进模式,Go语言服务模块在快速迭代中积累了大量业务逻辑。随着日均订单量突破千万级,原有测试策略暴露出明显瓶颈:单元测试覆盖率长期低于45%,集成测试依赖真实中间件(如Redis、MySQL)导致执行耗时超8分钟/次,端到端测试仅覆盖主路径且维护成本极高。这种“倒金字塔”结构使每次发布前回归验证成为高风险动作,线上P0级缺陷平均修复周期达11.3小时。
测试现状痛点分析
- 单元测试缺失边界用例与错误注入场景,mock粒度粗(常整包替换而非接口隔离)
- 集成测试环境与生产配置不一致,数据库使用Docker临时实例但未做事务回滚隔离
- E2E测试强耦合UI层,无法支撑API网关层灰度发布验证
重构核心目标
构建符合Go语言特性的三层测试金字塔:以接口契约驱动的单元测试为基座(目标覆盖率≥75%),轻量级容器化集成测试为支柱(单次执行≤90秒),精准场景化的契约测试为顶层(覆盖核心链路+异常熔断)。重点解决测试可重复性、环境一致性与反馈速度三大问题。
关键实施路径
采用testcontainers-go替代硬编码Docker调用,确保集成测试环境纯净:
// 示例:启动隔离的Redis实例用于订单缓存测试
func TestOrderCacheIntegration(t *testing.T) {
ctx := context.Background()
// 启动临时Redis容器(自动清理)
req := testcontainers.ContainerRequest{
Image: "redis:7-alpine",
ExposedPorts: []string{"6379/tcp"},
}
redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
require.NoError(t, err)
defer redisC.Terminate(ctx) // 容器生命周期自动管理
// 使用容器IP初始化客户端
ip, _ := redisC.Host(ctx)
port, _ := redisC.MappedPort(ctx, "6379")
client := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", ip, port.Port()),
})
// 执行缓存读写断言...
}
该方案将集成测试环境准备时间从3分钟压缩至8秒,且杜绝了端口冲突与数据残留问题。
第二章:单元测试层的深度重构实践
2.1 基于接口抽象与依赖倒置的可测性设计
可测性并非测试阶段的补救措施,而是架构设计的自然产物。核心在于将具体实现与行为契约解耦。
为什么接口是可测性的基石
- 隐藏内部状态与副作用,暴露确定性契约
- 允许在测试中注入模拟实现(Mock/Stub)
- 隔离被测单元与外部依赖(数据库、HTTP、时钟等)
依赖倒置的实践体现
class EmailService:
def send(self, to: str, content: str) -> bool: ...
class NotificationManager:
def __init__(self, emailer: EmailService): # ❌ 依赖具体类
self.emailer = emailer
# ✅ 改为依赖抽象
class EmailSender(Protocol):
def send(self, to: str, content: str) -> bool: ...
class NotificationManager:
def __init__(self, sender: EmailSender): # 依赖接口
self.sender = sender # 可注入 MockEmailSender 进行单元测试
逻辑分析:NotificationManager 不再创建或绑定 EmailService 实例,而是接收符合 EmailSender 协议的任意实现;参数 sender 是运行时注入的协作对象,使构造与使用分离,大幅提升测试可控性。
| 测试优势 | 说明 |
|---|---|
| 快速执行 | 替换真实邮件发送为内存操作 |
| 确定性结果 | Mock 可预设 send() 返回 True/False |
| 无副作用 | 避免真实发信、网络调用或日志污染 |
graph TD
A[NotificationManager] -->|依赖| B[EmailSender]
B --> C[RealEmailService]
B --> D[MockEmailSender]
D --> E[返回预设布尔值]
2.2 Go原生testing框架与testify/testify-suite协同演进
Go 原生 testing 包提供轻量、确定性的测试基础,而 testify/suite 则补足结构化生命周期管理能力,二者并非替代关系,而是职责互补的协同演进。
测试生命周期抽象
testify/suite 引入 SetupTest()/TearDownTest(),将重复逻辑从每个 TestXxx 中剥离:
type CalculatorSuite struct {
suite.Suite
calc *Calculator
}
func (s *CalculatorSuite) SetupTest() {
s.calc = NewCalculator() // 每次测试前重置状态
}
逻辑分析:
SetupTest在每个s.Run(...)子测试前执行;避免手动调用初始化,提升可维护性;suite.Suite内嵌*testing.T,保持原生断言兼容性。
协同优势对比
| 维度 | testing 原生 |
testify/suite |
|---|---|---|
| 状态隔离 | 手动管理(易遗漏) | 自动按测试粒度隔离 |
| 错误定位精度 | 行号+t.Errorf |
suite.Require() 提供堆栈增强 |
graph TD
A[go test] --> B[启动 testing.M]
B --> C[发现 TestXxx 函数]
C --> D{是否嵌入 suite.Suite?}
D -->|是| E[调用 SetupTest → Run → TearDownTest]
D -->|否| F[直接执行函数体]
2.3 表驱动测试在票务核心逻辑(如库存扣减、限购校验)中的规模化落地
为覆盖高维组合场景(用户等级 × 票种 × 库存状态 × 请求并发度),我们重构测试范式,以结构化用例表驱动全链路断言。
测试用例声明(YAML)
- name: "超限购票_白金用户_限购3张"
user_tier: "platinum"
ticket_type: "vip"
current_stock: 10
existing_orders: 3 # 已购3张,达上限
expect_error: "ERR_QUOTA_EXCEEDED"
expect_stock_after: 10 # 应无扣减
该配置被 TestTicketPurchase 统一加载,每条记录触发一次隔离事务执行与快照比对。
核心断言逻辑
func TestTicketPurchase(t *testing.T) {
for _, tc := range loadTestCases("purchase_cases.yaml") {
t.Run(tc.name, func(t *testing.T) {
// 构建上下文:预置库存、用户订单历史等
ctx := setupContext(tc)
_, err := Purchase(ctx, tc.ticket_type)
assert.Equal(t, tc.expect_error, extractErrorCode(err))
assert.Equal(t, tc.expect_stock_after, getStock(tc.ticket_type))
})
}
}
参数说明:
setupContext自动注入 mock DB 状态;extractErrorCode提取领域错误码而非 HTTP 状态,保障业务层契约一致性。
覆盖率提升对比
| 维度 | 手写测试用例 | 表驱动方案 |
|---|---|---|
| 新增用例耗时 | 15–25 分钟 | |
| 并发场景覆盖率 | 37% | 92% |
graph TD
A[用例表] --> B[DSL解析器]
B --> C[上下文工厂]
C --> D[事务化执行]
D --> E[多维度断言]
2.4 测试覆盖率精准归因:go tool cover + gocov分析瓶颈函数与边界路径
覆盖率采集与HTML可视化
执行以下命令生成细粒度覆盖率数据:
go test -coverprofile=coverage.out -covermode=count ./...
go tool cover -html=coverage.out -o coverage.html
-covermode=count 记录每行执行次数(非布尔标记),为后续热区定位提供量化依据;-html 输出交互式报告,支持逐文件钻取至行级高亮。
归因分析:识别低覆盖关键路径
使用 gocov 提取结构化数据并筛选瓶颈函数:
gocov convert coverage.out | gocov report -threshold=30
该命令输出未达30%覆盖的函数列表,并标注其所属文件与行号范围,直指边界条件缺失或异常分支未测场景。
覆盖率-复杂度关联参考表
| 函数名 | 行覆盖率 | Cyclomatic 复杂度 | 高风险路径数 |
|---|---|---|---|
ParseJSON |
42% | 8 | 3 |
ValidateInput |
67% | 12 | 5 |
根因定位流程
graph TD
A[执行 go test -covermode=count] --> B[生成 coverage.out]
B --> C[gocov convert → JSON]
C --> D[gocov report -threshold]
D --> E[定位低覆盖+高复杂度函数]
E --> F[检查对应测试用例的边界输入]
2.5 单元测试CI门禁建设:覆盖率阈值强制拦截与增量覆盖率报告集成
覆盖率门禁的触发逻辑
在 CI 流水线中,jacoco 生成的 exec 文件经 mvn jacoco:report 解析后,由 jacoco-check 插件执行阈值校验:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<id>check</id>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule implementation="org.jacoco.maven.RuleConfiguration">
<element>BUNDLE</element>
<limits>
<limit implementation="org.jacoco.maven.LimitConfiguration">
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.75</minimum> <!-- 全量覆盖率下限 -->
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
该配置强制要求指令覆盖率 ≥75%,低于则 mvn verify 失败并中断流水线。BUNDLE 级别作用于整个模块,避免单类豁免导致质量盲区。
增量报告的关键能力
仅全量阈值易掩盖腐化——新增代码未覆盖即被旧高覆盖率“稀释”。需结合 Git diff 计算增量行:
| 指标 | 全量覆盖率 | 增量覆盖率 | 说明 |
|---|---|---|---|
| 行覆盖(%) | 82.3 | 41.6 | 新增/修改行中仅41.6%被测 |
| 分支覆盖(%) | 68.9 | 29.0 | 高风险逻辑分支未触达 |
自动化集成流程
graph TD
A[Git Push] --> B[CI 触发]
B --> C[Checkout + Diff 获取变更文件]
C --> D[执行 mvn test -Djacoco.skip=false]
D --> E[Jacoco 生成 exec & report]
E --> F[插件解析 diff 范围内 class/line]
F --> G{增量行覆盖 ≥60%?}
G -->|否| H[Fail Build]
G -->|是| I[上传 HTML 报告至 Nexus]
第三章:服务集成层Mock策略体系化构建
3.1 基于Wire+GoMock的编译期Mock契约管理与自动化桩生成
传统单元测试中手动维护 mock 实现易导致接口变更时桩代码失效。Wire 与 GoMock 协同实现契约即代码:接口定义驱动 mock 生成,Wire 在编译期注入 mock 实例,保障依赖一致性。
核心工作流
- 定义
Repository接口(契约) - 运行
mockgen自动生成MockRepository - 通过 Wire
inject.go声明 mock 构建器 - 编译时 Wire 解析依赖图,替换真实实现为 mock
自动生成 mock 示例
mockgen -source=repository.go -destination=mocks/mock_repository.go -package=mocks
mockgen从repository.go提取所有导出接口,生成符合 GoMock 规范的桩结构体及预期方法(如EXPECT().GetUser()),支持泛型接口(Go 1.18+)。
Wire 注入配置节选
func NewMockAppSet() *App {
return wire.Build(
NewApp,
wire.Bind(new(Repository), new(*MockRepository)),
MockRepositorySet,
)
}
wire.Bind显式声明Repository接口由*MockRepository实现;MockRepositorySet是 mock 构造函数集合,确保编译期类型安全注入。
| 组件 | 职责 | 契约保障方式 |
|---|---|---|
| GoMock | 生成类型安全 mock 结构 | 接口签名反射校验 |
| Wire | 编译期依赖图解析与注入 | 类型约束 + 构造函数验证 |
mockgen CLI |
驱动 mock 代码生成 | 源码文件路径绑定 |
graph TD
A[interface.go] -->|mockgen| B[MockRepository.go]
B -->|Wire Bind| C[inject.go]
C -->|wire.Build| D[App with mocked deps]
3.2 HTTP/GRPC双模Mock:httptest.Server与grpc-go/testutil在分布式调用链中的协同验证
在微服务联调中,需同时验证 HTTP 接口与 gRPC 端点对同一业务逻辑的响应一致性。
双模Mock架构设计
- 复用同一业务 handler(如
UserService实例) - HTTP 层通过
httptest.NewUnstartedServer注册路由 - gRPC 层使用
testutil.NewServer启动无 TLS 的 in-process server
协同验证流程
// 启动双模Mock服务
httpSrv := httptest.NewUnstartedServer(httpMux)
grpcSrv := testutil.NewServer(t, &userService)
httpSrv.Start()
defer httpSrv.Close()
defer grpcSrv.Stop()
此代码启动隔离的 HTTP/gRPC 服务实例;
NewUnstartedServer允许手动注册中间件,testutil.NewServer自动处理 listener 和 goroutine 生命周期,参数t *testing.T用于失败时自动 cleanup。
| 维度 | HTTP Mock | gRPC Mock |
|---|---|---|
| 启动方式 | httptest.NewServer |
testutil.NewServer |
| 通信协议 | HTTP/1.1 | HTTP/2 + Protobuf |
| 调用链注入点 | X-Request-ID header |
metadata.MD |
graph TD
A[Client] -->|HTTP POST /user| B[httptest.Server]
A -->|gRPC GetUser| C[testutil.Server]
B & C --> D[Shared UserService]
D --> E[In-memory Store]
3.3 真实依赖轻量化替代:Redis/MQ本地嵌入式实例(miniredis、gomock-kafka)的选型与压测验证
在集成测试与CI流水线中,真实 Redis/Kafka 集群引入环境复杂度与延迟波动。miniredis(Go 实现的内存 Redis 兼容服务)与 gomock-kafka(轻量 Kafka broker 模拟器)成为主流替代方案。
压测对比维度
| 工具 | 启动耗时 | 内存占用 | Redis 命令覆盖率 | Kafka 协议支持 |
|---|---|---|---|---|
| miniredis v2.32 | ~4MB | 92%(含 Lua、Pub/Sub) | — | |
| gomock-kafka v0.8 | ~8ms | ~6MB | — | SASL/PLAIN + v2.8 API 子集 |
启动与注入示例
// miniredis 启动并注入 test client
srv := miniredis.RunT(t)
client := redis.NewClient(&redis.Options{
Addr: srv.Addr(), // 自动绑定 localhost:port
})
defer srv.Close() // 测试结束自动释放
逻辑说明:
miniredis.RunT(t)返回已就绪服务实例,Addr()提供动态端口;defer srv.Close()确保资源回收,避免端口泄漏。参数t用于测试生命周期绑定,无需手动管理 goroutine。
数据同步机制
graph TD
A[测试代码] --> B{调用 client.Set}
B --> C[miniredis 内存 store]
C --> D[响应写入完成]
D --> E[client.Get 触发 O(1) map 查找]
选型核心依据:协议保真度 > 启动速度 > 资源开销。
第四章:端到端与契约测试的工程化落地
4.1 基于OpenAPI 3.0的契约先行实践:Swagger Codegen自动生成客户端测试桩与断言模板
契约先行(Contract-First)要求接口定义早于实现,OpenAPI 3.0 YAML 是事实标准。Swagger Codegen 可据此生成可执行的测试桩与断言骨架。
自动生成测试桩结构
swagger-codegen generate \
-i api-spec.yaml \
-l java \
-o ./client-stub \
--additional-properties=sourceFolder=src/main/java,groupId=com.example,artifactId=api-stub
该命令解析 api-spec.yaml,生成含 ApiClient、PetApi 等类的 Java 客户端工程;--additional-properties 控制包结构与 Maven 元数据,确保可直接编译集成。
断言模板注入机制
| 模板类型 | 生成位置 | 用途 |
|---|---|---|
| ResponseAssert | src/test/java/.../asserts/ |
封装 statusCode(), bodyContains() 等 Fluent 断言方法 |
| MockServerStub | src/test/resources/mock/ |
内置 WireMock 配置,按路径+状态码预设响应 |
流程协同示意
graph TD
A[OpenAPI 3.0 YAML] --> B[Swagger Codegen]
B --> C[客户端SDK]
B --> D[测试桩 + 断言模板]
C --> E[集成测试调用]
D --> E
4.2 大麦高并发场景下的E2E测试分层调度:基于Ginkgo BDD的场景编排与超时熔断机制
在大麦票务秒杀峰值(QPS > 50k)下,传统串行E2E测试常因单点超时导致整套用例阻塞。我们采用Ginkgo的BeforeEach+AfterEach生命周期钩子实现三层调度隔离:
- 基础服务层(DB/Cache)→ 独立资源池 + 连接复用
- 业务中台层(选座/锁座)→ 按流量比例动态分片
- 渠道网关层(小程序/H5)→ 请求头标识路由+熔断降级
超时熔断策略配置
// ginkgo-suite.go:每个Describe块绑定独立熔断器
var _ = Describe("SeatLockFlow", func() {
var circuitBreaker *gobreaker.CircuitBreaker
BeforeEach(func() {
circuitBreaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "seat-lock",
Timeout: 3 * time.Second, // 熔断触发阈值
ReadyToTrip: cbFunc, // 连续3次失败即熔断
OnStateChange: logStateChange,
})
})
})
Timeout=3s适配锁座接口P99延迟(2.1s),ReadyToTrip回调函数统计最近10次调用错误率,超60%则进入半开状态。
分层调度效果对比
| 层级 | 平均耗时 | 失败率 | 熔断触发频次 |
|---|---|---|---|
| 基础服务层 | 87ms | 0.02% | 0 |
| 业务中台层 | 1.2s | 1.8% | 2次/小时 |
| 渠道网关层 | 2.4s | 5.3% | 17次/小时 |
graph TD
A[测试启动] --> B{QPS > 30k?}
B -->|是| C[启用分片调度]
B -->|否| D[直连全量集群]
C --> E[网关层熔断器拦截异常请求]
E --> F[自动降级至缓存兜底]
4.3 票务全链路数据一致性验证:MySQL Binlog监听+Testcontainers构建最终一致性断言流水线
数据同步机制
采用 Debezium MySQL Connector 实时捕获 Binlog,将订单创建、支付、出票等 DML 变更转化为结构化事件流:
// Debezium 配置片段(嵌入 Testcontainers)
Map<String, String> debeziumProps = Map.of(
"database.hostname", "mysql",
"database.port", "3306",
"database.user", "debezium",
"database.password", "dbz",
"database.server.id", "184054",
"table.include.list", "ticket_db.orders,ticket_db.tickets" // 精确订阅关键表
);
该配置确保仅监听票务核心表变更,降低网络与序列化开销;server.id 避免 MySQL 主从复制冲突,table.include.list 实现语义级过滤。
流水线编排
Testcontainers 启动 MySQL + Kafka + Debezium 组合容器栈,自动拉起端到端环境:
| 组件 | 版本 | 作用 |
|---|---|---|
| MySQL | 8.0.33 | 源数据库(含初始化数据) |
| Confluent Kafka | 7.4.0 | 事件中转与持久化 |
| Debezium | 2.4.0 | Binlog 解析与 CDC 投递 |
一致性断言逻辑
await().atMost(30, SECONDS)
.untilAsserted(() -> {
List<TicketEvent> kafkaEvents = consumeFromTopic("ticket_db.tickets");
Ticket dbState = jdbcTemplate.queryForObject(
"SELECT * FROM tickets WHERE id = ?",
new BeanPropertyRowMapper<>(Ticket.class), "T2024001"
);
assertThat(kafkaEvents).extracting("status").contains("ISSUED");
assertThat(dbState.getStatus()).isEqualTo("ISSUED"); // 最终一致断言
});
通过 await().untilAsserted() 实现弹性等待,兼容异步传播延迟;consumeFromTopic 封装 Kafka Consumer 重试与 offset 管理逻辑,确保事件可见性。
graph TD A[MySQL Order Insert] –>|Binlog| B(Debezium Connector) B –>|Avro Event| C[Kafka Topic: orders] C –> D[Stream Processor] D –>|Upsert| E[(Tickets Table)] E –>|SELECT| F[Consistency Assertion]
4.4 测试金字塔可视化看板:Prometheus+Grafana聚合各层测试通过率、耗时、覆盖率趋势指标
数据同步机制
测试框架(JUnit/TestNG/Pytest)通过 prometheus-client 暴露指标端点,关键指标命名遵循语义化规范:
# test_metrics.py
from prometheus_client import Counter, Histogram, Gauge
# 各层测试结果(标签区分 unit/integration/e2e)
test_pass_total = Counter(
'test_pass_total',
'Total passed tests',
['layer', 'suite'] # layer: unit/integration/e2e
)
# 耗时直方图(自动分桶)
test_duration_seconds = Histogram(
'test_duration_seconds',
'Test execution time in seconds',
['layer']
)
→ 逻辑说明:['layer'] 标签实现测试金字塔分层维度切片;Histogram 自动记录 0.005~10s 十余档延迟分布,支撑 P95/P99 耗时分析。
指标聚合视图
Grafana 面板配置核心查询(PromQL):
| 面板类型 | 查询表达式 | 用途 |
|---|---|---|
| 通过率趋势 | rate(test_pass_total{layer=~"unit|integration|e2e"}[1h]) / ignoring(result) group_left() rate(test_total{layer=~"unit|integration|e2e"}[1h]) |
分层通过率(%)时序线图 |
| 覆盖率热力图 | avg_over_time(coverage_percent{layer="unit"}[7d]) |
单元测试覆盖率周均值 |
看板联动逻辑
graph TD
A[CI流水线] -->|push metrics| B[Prometheus scrape]
B --> C[指标存储]
C --> D[Grafana Dashboard]
D --> E[分层钻取:unit → integration → e2e]
第五章:重构成果复盘与Go测试工程化演进路线
重构前后关键指标对比
| 指标项 | 重构前(v1.2) | 重构后(v2.5) | 变化幅度 |
|---|---|---|---|
| 单元测试覆盖率 | 42% | 86% | +44% |
go test -race 报告数据竞争数 |
7处 | 0处 | 100%消除 |
| CI流水线平均耗时 | 6m 23s | 2m 18s | ↓65% |
| HTTP Handler层Mock依赖数量 | 12个 | 3个(仅保留核心接口) | ↓75% |
核心重构策略落地实录
在订单服务模块中,我们将原单体式 OrderService 拆分为 OrderValidator、InventoryAdapter 和 NotificationPublisher 三个独立组件,并通过接口契约解耦。重构后,TestPlaceOrder_Success 用例从原先依赖真实Redis和RabbitMQ,转变为使用 gomock 生成的 InventoryClientMock 与 NotifierMock,执行时间由 3.2s 缩短至 187ms。关键代码片段如下:
func TestPlaceOrder_Success(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockInventory := mocks.NewMockInventoryClient(mockCtrl)
mockInventory.EXPECT().Deduct(gomock.Any(), "SKU-001", int64(2)).Return(nil)
svc := NewOrderService(mockInventory, mocks.NewMockNotifier(mockCtrl))
// ... 断言逻辑
}
测试工程化四阶段演进路径
flowchart LR
A[阶段一:手工验证] --> B[阶段二:基础单元测试覆盖]
B --> C[阶段三:集成测试+TestMain初始化]
C --> D[阶段四:测试即服务:自动生成桩、覆盖率门禁、失败用例自动归档]
自动化测试基建升级细节
我们引入 testground 框架统一管理测试生命周期,在 TestMain 中注入全局配置:连接池预热、etcd测试集群启动、Prometheus mock server 启动。同时将 go test -coverprofile=coverage.out 集成至GitHub Actions,当覆盖率低于85%时自动阻断PR合并。CI日志中新增结构化测试元数据输出:
{
"test_suite": "order_service",
"passed": 47,
"failed": 0,
"flaky": 0,
"coverage_percent": 86.3,
"critical_path_duration_ms": 1247
}
真实故障拦截案例
2024年Q2一次上线前回归中,新引入的 RefundProcessor 在并发调用下触发 sync.Map.LoadOrStore 的竞态条件。该问题被 go test -race -count=5 在第3轮随机执行中捕获,日志显示:
WARNING: DATA RACE
Write at 0x00c0001a2080 by goroutine 12:
github.com/ourcorp/payment.(*RefundProcessor).processRefund()
refund.go:89 +0x21a
Previous read at 0x00c0001a2080 by goroutine 9:
github.com/ourcorp/payment.(*RefundProcessor).getCacheKey()
refund.go:62 +0x1a3
团队据此将 sync.Map 替换为带读写锁的 cacheMap 结构体,避免线上资金异常。
工程文化配套措施
每周四设立“测试共建日”,由SRE牵头组织用例评审;所有新增业务逻辑必须附带边界值测试(如负库存、超长字符串、空指针输入);internal/testutil 包已沉淀37个可复用工具函数,包括 MustTempDB()、FakeHTTPServer()、CaptureLogOutput() 等。
