一键导入
fosmvvm-viewmodel-test-generator
Generate ViewModel tests with codable round-trip, versioning stability, and multi-locale translation verification.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Generate ViewModel tests with codable round-trip, versioning stability, and multi-locale translation verification.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Discover FOSUtilities APIs before writing helper code. Use when reaching for JSON/Codable encoding-decoding, wire-format date formatting, URLSession/HTTP fetching, WebSockets, network mocking in tests, collection grouping or throttled iteration, string casing/hashing/obfuscation/CSV parsing, hex or rounded-number formatting, semantic version comparison, semaphores or async-from-sync bridging, typed model identifiers, runtime environment/bundle-version checks (simulator/TestFlight detection), ViewModel declaration and localization, form fields and validation, ServerRequests/CRUD, SwiftUI ViewModel binding, Vapor boot wiring/routes/Fluent/Leaf/middleware, ViewModel or ServerRequest or UI testing, or PDF generation — in any project that imports FOSFoundation, FOSMVVM, FOSMVVMVapor, FOSTesting, or FOSReporting.
Update the FOSUtilities API catalog after API changes. Runs the symbol-graph audit, fixes stale entries, writes curated entries for gaps, maintains the reach-for index, and bumps the plugin version. Use in the FOSUtilities repo when CI's catalog audit warns/fails, after adding or renaming public API, or when a reach-for index line is missing.
Generate FOSMVVM Fields protocols with validation rules, FormField definitions, and localized messages. Define form contracts once, validate everywhere.
Generate Fluent DataModels for FOSMVVM server-side persistence. Scaffolds models, migrations, and tests for database-backed entities.
Generate Leaf templates for FOSMVVM WebApps. Create full-page views and HTML-over-the-wire fragments that render ViewModels.
Plan FOSMVVM implementation work with a design-first, DocC-first gate. Use BEFORE writing an implementation plan that adds or changes FOSMVVM types, protocols, ViewModels, requests, identities, or persistence — it applies FOSMVVM's design and documentation discipline up front, then hands task decomposition to your generic plan process.
| name | fosmvvm-viewmodel-test-generator |
| description | Generate ViewModel tests with codable round-trip, versioning stability, and multi-locale translation verification. |
| homepage | https://github.com/foscomputerservices/FOSUtilities |
| metadata | {"clawdbot":{"emoji":"🔬","os":["darwin","linux"]}} |
Generate test files for ViewModels following FOSMVVM testing patterns.
For full architecture context, see FOSMVVMArchitecture.md | OpenClaw reference
API catalog: check
../shared/api-catalog/FOSTesting.md§ FOSTesting before hand-writing helpers.
ViewModel testing in FOSMVVM verifies three critical aspects:
@LocalizedString properties have values in all supported localesThe LocalizableTestCase protocol provides infrastructure that tests all three in a single call.
The version baseline is a COMMITTED artifact (in a downstream app). Versioning stability works by comparing the ViewModel's current serialization against a stored baseline JSON (
{Name}ViewModel_<version>.json, written beside the test underTests/.../.VersionedTestJSON/). For an app that ships a versioned wire contract to real clients, commit that baseline to git — it is the canary that catches an accidental wire-shape change across builds. If it isn't committed, it regenerates fresh every clean build and silently protects nothing. (Different policy for FOSUtilities itself: its own baselines are regenerable fixtures with no shipped contract — the version tags are arbitrary — so FOS deliberately git-ignores them except one intentional serialization canary. Downstream apps are the opposite: commit real baselines.) The primaryexpectFullViewModelTests(_:)now forwards#filePath/#line, so the baseline lands beside your test (underTests/.../.VersionedTestJSON/) automatically — commit that file.
@LocalizedSubs substitution behavior| File | Location | Purpose |
|---|---|---|
{Name}ViewModelTests.swift | Tests/{Target}Tests/Localization/ | Test suite conforming to LocalizableTestCase |
{Name}ViewModel.yml | Tests/{Target}Tests/TestYAML/ | YAML translations for test (if needed) |
For most ViewModels, a single line provides complete coverage:
@Test func dashboardViewModel() throws {
try expectFullViewModelTests(DashboardViewModel.self)
}
This verifies:
This is sufficient for the vast majority of ViewModel tests.
When testing specific formatting behavior (substitutions, compound strings), add locale-specific assertions:
@Test func greetingWithSubstitution() throws {
try expectFullViewModelTests(GreetingViewModel.self)
// Verify specific substitution behavior
let vm: GreetingViewModel = try .stub()
.toJSON(encoder: encoder(locale: en))
.fromJSON()
#expect(try vm.welcomeMessage.localizedString == "Welcome, John!")
}
This is optional - use only when verifying specific formatting techniques.
The expect* helpers verify behavior the contract guarantees — Codable round-trip, version stability, translations exist. Keep any added assertions at that same altitude. (Background: Architecture Patterns → Encapsulation Is the Precondition; repo CLAUDE.md → Encapsulation Is the Precondition SOLID Assumes.)
obj["someKey"], "encodes as a bare string", exact byte layout, key order). There is no contract that a type encodes a particular way — only that it round-trips. Assert round-trip identity-preservation and behavior, never the shape. A representation assertion freezes an implementation detail and breaks on a harmless format change..stub(), public inits, and the try value.toJSON(encoder:).fromJSON() round-trip. @testable import here exists to see internal ViewModel types and test infrastructure — it is not a license to reach a value's private/internal init or getter to fabricate or inspect state the public contract doesn't offer. @testable/private access is legitimate only for block/arc coverage, never for contract coverage. If a value type seals its backing (no public accessor), assert via == / hashValue / Comparable, not by reading internals.expectVersionedViewModel is decode-only — never "regenerate" baselines to make a change pass. It writes a baseline once (if absent), then only re-decodes every committed version to prove old encodings still load; it does not encode-diff, and it skips ClientHostedViewModelFactory types. So a schema change must stay backward-decodable (add fields, don't rename/remove). Deleting the committed .VersionedTestJSON files to "refresh" them destroys the historical versions that are the entire point — don't. New fields are additive; old baselines must keep decoding.Date/timestamp is not equal to itself after a round-trip (sub-ms is truncated). Don't assert in-memory Date equality across encode→decode; compare via behavior/ordering, or start from an already-canonical fixture.Test suites conform to LocalizableTestCase to access testing infrastructure:
import FOSFoundation
@testable import FOSMVVM
import FOSTesting
import Foundation
import Testing
@testable import {ViewModelsTarget}
@Suite("My ViewModel Tests")
struct MyViewModelTests: LocalizableTestCase {
let locStore: LocalizationStore
init() throws {
self.locStore = try Self.loadLocalizationStore(
bundle: {ViewModelsTarget}.resourceAccess,
resourceDirectoryName: ""
)
}
}
The {ViewModelsTarget}.resourceAccess is the resource accessor defined when creating the ViewModels SPM target (via FOSResourceAccessor build tool plugin).
| Property/Method | Purpose |
|---|---|
locStore | Required - the localization store |
locales | Optional - locales to test (default: en, es) |
encoder(locale:) | Creates a localizing JSONEncoder |
en, es, enGB, enUS | Locale constants |
| Method | Use When |
|---|---|
expectFullViewModelTests(_:) | Primary - complete ViewModel testing |
expectTranslations(_:) | Translation-only verification |
expectFullFieldValidationModelTests(_:) | Testing FieldValidationModel types |
expectFullFormFieldTests(_:) | Testing FormField instances |
expectCodable(_:encoder:) | Codable round-trip only |
expectVersionedViewModel(_:encoder:) | Versioning stability only |
Every ViewModel with @LocalizedString properties needs YAML entries:
@ViewModel
public struct DashboardViewModel: RequestableViewModel {
@LocalizedString public var pageTitle // Needs YAML entry
@LocalizedString public var emptyMessage // Needs YAML entry
public let itemCount: Int // No YAML needed
}
# DashboardViewModel.yml
en:
DashboardViewModel:
pageTitle: "Dashboard"
emptyMessage: "No items yet"
es:
DashboardViewModel:
pageTitle: "Tablero"
emptyMessage: "No hay elementos todavía"
When a ViewModel contains child ViewModels, all types in the hierarchy need YAML entries:
@ViewModel
public struct BoardViewModel: RequestableViewModel {
@LocalizedString public var title
public let cards: [CardViewModel] // Child ViewModel
}
@ViewModel
public struct CardViewModel {
@LocalizedString public var cardTitle
}
Both BoardViewModel and CardViewModel need YAML entries (can be in same or separate files).
When tests define private ViewModel structs for testing specific scenarios, those also need YAML:
// In test file
private struct TestParentViewModel: ViewModel {
@LocalizedString var title
let children: [TestChildViewModel]
}
private struct TestChildViewModel: ViewModel {
@LocalizedString var label
}
Add entries to a test YAML file for these private types.
Invocation: /fosmvvm-viewmodel-test-generator
Prerequisites:
Workflow integration: This skill is used when adding test coverage for ViewModels. The skill references conversation context automatically—no file paths or Q&A needed. Typically follows fosmvvm-viewmodel-generator.
This skill references conversation context to determine test structure:
From conversation context, the skill identifies:
Verifies completeness:
Creates test suite with:
Skill references information from:
See reference.md for complete file templates.
@Test func dashboardViewModel() throws {
try expectFullViewModelTests(DashboardViewModel.self)
}
@Test func boardViewModels() throws {
try expectFullViewModelTests(BoardViewModel.self)
try expectFullViewModelTests(ColumnViewModel.self)
try expectFullViewModelTests(CardViewModel.self)
}
var locales: Set<Locale> { [en, es, enGB] } // Override default
@Test func multiLocaleViewModel() throws {
try expectFullViewModelTests(MyViewModel.self)
// Tests en, es, AND en-GB
}
@Test func greetingSubstitutions() throws {
try expectFullViewModelTests(GreetingViewModel.self)
let vm: GreetingViewModel = try .stub(userName: "Alice")
.toJSON(encoder: encoder(locale: en))
.fromJSON()
#expect(try vm.welcomeMessage.localizedString == "Welcome, Alice!")
}
@Test func parentWithChildren() throws {
// Tests parent AND verifies children can be encoded/decoded
try expectFullViewModelTests(ParentViewModel.self)
// Optionally verify specific child values
let vm: ParentViewModel = try .stub()
.toJSON(encoder: encoder(locale: en))
.fromJSON()
#expect(try vm.children[0].label.localizedString == "Child 1")
}
FOSLocalizableError: _pageTitle -- Missing Translation -- en
Cause: YAML entry missing for a @LocalizedString property.
Fix: Add the property to the YAML file:
en:
MyViewModel:
pageTitle: "Page Title" # Add this
Cause: The ViewModel wasn't encoded with a localizing encoder.
Fix: Ensure using encoder(locale:) or expectFullViewModelTests().
Cause: YAML values exist but may have typos or wrong content.
Fix: Add specific assertions to verify exact values:
let vm = try .stub().toJSON(encoder: encoder(locale: en)).fromJSON()
#expect(try vm.title.localizedString == "Expected Value")
| Concept | Convention | Example |
|---|---|---|
| Test suite | {Feature}ViewModelTests | DashboardViewModelTests |
| Test file | {Feature}ViewModelTests.swift | DashboardViewModelTests.swift |
| YAML file | {ViewModelName}.yml | DashboardViewModel.yml |
| Test method | {viewModelName}() or descriptive | dashboardViewModel() |
vmId equality/stability), never an incidental encoded shape, and never reaches into a sealed value's internals to assert them — that's the encapsulation break from the test side. Repo CLAUDE.md → Encapsulation Is the Precondition SOLID Assumes.| Version | Date | Changes |
|---|---|---|
| 1.0 | 2025-01-02 | Initial skill |
| 1.1 | 2026-01-19 | Updated LocalizableTestCase example to use {ViewModelsTarget}.resourceAccess pattern. |
| 1.2 | 2026-01-24 | Update to context-aware approach (remove file-parsing/Q&A). Skill references conversation context instead of asking questions or accepting file paths. |
| 1.3 | 2026-07-02 | Note the version baseline is a committed artifact for downstream apps (FOS's own baselines are regenerable/git-ignored fixtures — different policy); expectFullViewModelTests(_:) now forwards #filePath/#line so the baseline lands beside the caller's test. (backlog B7) |