Skip to content

test: establish automated test infrastructure for GrowingAnalytics core modules - #45

Merged
YoloMao merged 30 commits into
masterfrom
test/tdd-infrastructure
Apr 9, 2026
Merged

test: establish automated test infrastructure for GrowingAnalytics core modules#45
YoloMao merged 30 commits into
masterfrom
test/tdd-infrastructure

Conversation

@YoloMao

@YoloMao YoloMao commented Apr 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

为 GrowingAnalytics SDK 建立本地单元测试基础设施,覆盖核心模块的纯逻辑路径,可在 DevEco Studio 中无设备直接运行。

测试覆盖

模块 测试文件 覆盖内容
GrowingConfig interfaces/GrowingConfig.test.ets 三种模式(NewSaaS/SaaS/CDP)参数校验、配置项默认值、copy() 验证逻辑、IgnoreFields 位掩码
Util utils/Util.test.ets 序列化协议(V2/V3)、XOR 加密、属性类型转换、事件大小校验
NiceTry utils/NiceTry.test.ets niceTry / niceTryAsync 异常捕获与 fallback
EventPersistence event/EventPersistence.test.ets fromDatabase 字段保留、大事件降级、V2/V3 序列化格式
EventSendCycle event/EventSendCycle.test.ets 上报调度决策:2xx 删除事件、4xx/5xx 保留、空库跳过、异常传播
Network core/Network.test.ets generateUrl(accountId 替换、stm 参数)、generateHeaders(加密/压缩请求头)
Session core/Session.test.ets sessionId 查询、多 Tracker 隔离、超时判断
EventTimer core/EventTimer.test.ets 计时状态机、pause/resume/clear、24h 边界保护
GeneralProps core/GeneralProps.test.ets set/remove/clear/dynamic 操作及组合场景、动态属性优先级、副本隔离
GrowingContext core/Context.test.ets contexts Map 增删查、defaultContext 管理、hasSimilarContext 双维度去重、SaaS 双 EventSender
UserIdentifier core/UserIdentifier.test.ets userId 长度校验、幂等性、清空、idMapping 控制;A→B 切换触发 session 刷新;CDP 空→有触发 generateVisit
DeviceInfo core/DeviceInfo.test.ets isNotIgnoreField():SaaS/CDP 忽略 ignoreField 始终返回 true;NewSaaS 单字段、多字段组合、IgnoreFieldsAll 过滤行为
EventBuilder event/EventBuilder.test.ets sequenceId 自增与 accountId+dataSourceId 隔离、timestamp 回退、appState、sessionId、userId/userKey、location 过滤、attributes 合并、config 字段透传、sdkVersion
Event event/EventBuilder.test.ets toSerialize() 三路模式分发:SaaS→V2 / CDP→V3 / NewSaaS→JSON.stringify
PageEvent event/PageEvent.test.ets lastPage 状态机(trackerId 隔离)、SaaS/NewSaaS referralPage 链路、CDP 不写 lastPage
CustomEvent event/CustomEvent.test.ets SaaS 模式 lastPage 注入(path / pageShowTimestamp);NewSaaS/CDP 不注入
AppClosedEvent event/CustomEvent.test.ets SaaS 模式 lastPage → event.path 注入;非 SaaS 不注入
ViewElementEvent event/ViewElementEvent.test.ets CDP pageShowTimestamp(>0 保留 / =0 回退 Date.now() / 非CDP→undefined);ViewClick textValue 过滤;index>0 才写入
Queue mobileDebugger/Queue.test.ets enqueue/dequeue 基础流程、dequeue 快照隔离、超出 limitSize 丢弃最旧条目、limitSize≤0 回退默认值 50

测试辅助设施

  • JsonParser.ts 文件):封装 JSON.parse()any 类型处理,向 .ets 测试文件暴露类型安全的访问接口,规避 arkts-no-any-unknown 限制
  • TestBuildersbuildFakeContext() / makeFakePersistence() 工厂函数,快速构建测试用 GrowingContext 和 EventPersistence 实例

生产代码变更

EventSender null 防御(fix)

sendEvent() 原先通过 as GrowingContext 强转掩盖了 GrowingContext.getContext() 可能返回 undefined 的情况,导致 context 被清理后回调仍触发时访问 context.config 崩溃。改为在加锁前做显式 null 检查,context 不存在时静默返回,isUploading 保持 false,不影响后续调用。

EventSender 重构

sendEvent() 中的上报调度决策逻辑提取为独立的纯函数 runSendCycle(io: SendCycleIO),通过 SendCycleIO 接口注入 IO 依赖(fetchEvents / sendEvents / removeEvents / afterRemove / countRemaining),使调度逻辑可脱离平台 API 独立测试。原 sendEvent() 方法改为构造 SendCycleIO 实例后委托调用,行为不变。

测试隔离支持

SessionEventTimerUserIdentifierEventBuilderGeneralPropsGrowingContextPageEvent 七个静态单例类添加 _reset() 方法,用于测试用例间重置静态状态,避免测试污染。_reset 符号已加入 consumer-rules.txt 混淆白名单。

依赖与配置

  • oh-package.json5 添加 devDependencies@ohos/hypium@1.0.21@ohos/hamock@1.0.2

测试运行

在 DevEco Studio 中通过 Run Configurations 选择 GrowingAnalyticsTest 运行,无需连接设备。

命令行编译验证:

/Applications/DevEco-Studio.app/Contents/tools/node/bin/node \
  /Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js \
  --mode module -p product=default -p module=GrowingAnalytics@default \
  UnitTestBuild --analyze=normal --parallel --incremental --daemon

🤖 Generated with Claude Code

YoloMao and others added 30 commits April 7, 2026 11:44
- UserIdentifier._reset(): replace `{}` with `JSON.parse('{}') as object`
  to satisfy arkts-no-untyped-obj-literals for `object`-typed field
- EventTimerTest: fix durationFrom 24h boundary tests to use startTime>0
  so they actually exercise the duration guard, not the startTime guard
- InMemoryEventStore: replace `new TextEncoder()` (not a global in ArkTS)
  with `Util.sizeOfEventString()` for byte counting
- LocalUnit.test.ets: remove unused `{ describe, it, expect }` import
- UtilTest: define top-level interfaces (CustomEventData, PageEventData,
  VisitEventData, AppClosedEventData, V3*EventData, ValidateSize*Event)
  and replace all `Record<string, object>` inline literals
- EventPersistenceTest: add top-level interfaces for all inline event
  objects passed to Util.validateEventSize / toSerializeByMeasurementProtocol*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NiceTryTest: change Record<string,object> to Record<string,string>
  so niceTry<string> type T unifies correctly with string fallback
- EventTimerTest/SessionTest: replace `as GrowingContext` with
  `as unknown as GrowingContext` for bare object literals (required
  for non-overlapping class type casts in ArkTS strict mode)
- EventTimerTest: expand shorthand `{ trackerId }` to `{ trackerId: trackerId }`
- TestBuilders/NetworkTest: expand shorthand `config` to `config: config`
  (shorthand property initializer has no precedent in production ArkTS code)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All test files now pass UnitTestBuild compilation with zero errors:
- Replace `as unknown as T` with `as ESObject as T` throughout
- Define named interfaces for all inline object type declarations
  (NetworkTestOptions, FakeContextOptions, MinimalContext variants)
- Move nested helper functions (makeBase, makePageEvent) to module level
- Cast all JSON.parse() results to ESObject
- Replace Record<string,string> with AttributesType (TypeScript-sourced
  index-signature type that accepts object literals in ArkTS)
- Replace non-existent assertNotContain() with .includes() + assertFalse()
- Simplify FakeNetwork.responses to number[] and introduce FakeRcpResponse
  class to avoid untyped rcp.Response construction
- Expand shorthand object properties to explicit key: value form

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In the full local-test build mode (-p isLocalTest=true -p unitTestMode=true
-p ohos-test-coverage=true), JSON.parse() returns unknown even when cast
to ESObject, triggering arkts-no-any-unknown errors.

Root cause: this stricter pipeline treats ESObject like any/unknown.

Fix: introduce JsonParser.ts (TypeScript, not ArkTS), which wraps
JSON.parse() internally and exposes typed getString/getNumber/hasKey
accessors. The unknown type is contained inside the .ts file and never
leaks into .ets consumers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- rename all XxxTest.ets to Xxx.test.ets (e.g. UtilTest.ets → Util.test.ets)
- declare all test suites directly in List.test.ets instead of delegating to localUnitTest()
- reset LocalUnit.test.ets to boilerplate placeholder (same style as Autotrack.test.ets)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
replace all describe() names containing Chinese characters, spaces,
hyphens, parentheses, or slashes with ASCII-only ClassName_featureName
format to satisfy hypium's constraint (only letters, digits, underscores,
and periods, starting with a letter)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ardless of null value

xcontent 为 null 时条件判断提前退出,导致 null 值字段被保留在序列化结果中。
改为无条件 delete,与 timezoneOffset 处理方式一致。

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
hypium 中 beforeEach 仅对其所在 describe 块内的 it 生效,
定义在 describe 外部时不会在嵌套 describe 的用例间执行。
将 beforeEach 移入各 describe 块内,确保每个用例前正确重置静态状态。

涉及文件:Session.test.ets、EventTimer.test.ets、EventSender.test.ets

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…tability

Decouple the send-cycle scheduling logic from platform IO (EventDatabase,
Network, Plugins) via a SendCycleIO callback interface, enabling unit tests
without HarmonyOS runtime dependencies. Remove unused interface abstractions
(IEventStore, INetwork, IStorage) and their test doubles in favor of the
lighter callback-injection approach.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…usages

ArkTS strict mode requires explicit return types on arrow functions
(arkts-no-implicit-return-types). Also add missing afterRemove callback
to all test SendCycleIO object literals.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SDK 的 toSerializeByMeasurementProtocolV3 仅在 xcontent 有实际值时移除,
xcontent 为 null 时 JSON.stringify 会保留该字段,测试断言应与此一致。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eset

EventSender 构造时注册了 emitter.once(EMIT_EVENT_DATABASE_FLUSH) 回调,
当 GrowingContext._reset() 后 contexts Map 被清空,该回调仍然存活。
触发时 GrowingContext.getContext() 返回 undefined,原代码通过
as GrowingContext 强转掩盖了类型,导致后续 context.config 访问崩溃。

改为在 isUploading 锁之后、try 块之前做显式 null 检查,
context 不存在时静默 return,isUploading 保持 false 不影响后续调用。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
新增三个核心模块的本地单元测试,补齐数据准确性关键路径的覆盖:

GeneralProps(35 用例)
- set/remove/clear/dynamic 四类操作及组合场景
- 动态属性优先级高于静态属性(同名覆盖)
- getGeneralProps 返回副本,外部修改不影响内部状态

GrowingContext(20 用例)
- contexts Map 的增删查及重复 set 覆盖语义
- defaultContext 设置、MAIN_TRACKER_ID 固定为 __GrowingAnalyticsCore
- hasSimilarContext 按 trackerId / accountId+dataSourceId 双维度去重
- SaaS 模式 EventSender 数量(PV + CSTM = 2 个)

UserIdentifier(25 用例)
- initUser / initMultiUser 在 Preferences 未初始化时返回空字符串
- setLoginUserId:>1000 字符静默忽略、幂等性、清空、idMapping 控制
- A→B 用户切换触发 Session.refreshSession(sessionId 变化可观测)
- CDP 模式:空→有触发 generateVisit;A→B 走 refreshSession
- 使用 dataCollectionEnabled=false 阻断 AnalyticsCore.writeEventToDisk

同时为 GeneralProps 和 GrowingContext 补充 _reset() 测试隔离方法,
与 Session、EventTimer 保持一致的测试基础设施规范。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…wElementEvent

- EventBuilder: sequenceId 自增与隔离、timestamp 回退、appState、
  sessionId、userId/userKey、location 过滤、attributes 合并、
  config 字段透传、sdkVersion
- Event.toSerialize(): 三路模式分发(SaaS→V2 / CDP→V3 / NewSaaS→JSON.stringify)
- PageEvent: lastPage 状态机(trackerId 隔离)、SaaS/NewSaaS referralPage 链路、
  CDP 不写 lastPage;新增 PageEvent._reset() 供测试隔离
- CustomEvent / AppClosedEvent: SaaS 模式 lastPage 注入(path / pageShowTimestamp)
- ViewElementEvent: CDP pageShowTimestamp、ViewClick textValue 过滤、
  index > 0 过滤

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- DeviceInfo.isNotIgnoreField():SaaS/CDP 模式忽略 ignoreField 始终返回 true;
  NewSaaS 模式下单字段、多字段组合掩码、IgnoreFieldsAll 的过滤行为
- Queue<T>:基础入队/出队、dequeue 清空并返回快照、超出 limitSize 时
  丢弃最旧条目、limitSize ≤ 0 回退默认值 50

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… in SendCycleIO

sendEvents now returns SendResult (statusCode + request) and runSendCycle
passes request as a parameter to afterRemove, eliminating the mutable
closure coupling between callbacks and ensuring onEventsDidSend is never
silently skipped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@YoloMao
YoloMao force-pushed the test/tdd-infrastructure branch from 514fb66 to 8523758 Compare April 9, 2026 07:14
@YoloMao
YoloMao merged commit 0be5f34 into master Apr 9, 2026
1 check passed
@YoloMao
YoloMao deleted the test/tdd-infrastructure branch April 9, 2026 07:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant