Skill: foscomputerservices/fosmvvm-viewmodel-test-generator (version 2.0.6)

--- 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"]}} --- # FOSMVVM ViewModel Test Generator Generate test files for ViewModels following FOSMVVM testing patterns. ## Conceptual Foundation > For full architecture context, see [FOSMVVMArchitecture.md](../../docs/FOSMVVMArchitecture.md#testing-support) | [OpenClaw reference]({baseDir}/references/FOSMVVMArchitecture.md) ViewModel testing in FOSMVVM verifies three critical aspects: 1. **Codable round-trip** - ViewModel encodes and decodes without data loss 2. **Versioning stability** - Structure hasn't changed unexpectedly 3. **Multi-locale translations** - All `@LocalizedString` properties have values in all supported locales The `LocalizableTestCase` protocol provides infrastructure that tests all three in a single call. --- ## When to Use This Skill - Creating tests for new ViewModels - Adding test coverage to existing ViewModels - Verifying localization completeness across locales - Testing ViewModels with embedded/nested child ViewModels - Verifying `@LocalizedSubs` substitution behavior ## What This Skill Generates | 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) | --- ## The Testing Pattern ### Standard Pattern (Most Tests) For most ViewModels, a single line provides complete coverage: ```swift @Test func dashboardViewModel() throws { try expectFullViewModelTests(DashboardViewModel.self) } ``` This verifies: - Codable encoding/decoding - Versioned ViewModel stability - Translations exist for all locales (en, es by default) **This is sufficient for the vast majority of ViewModel tests.** ### Extended Pattern (Specific Formatting Verification) When testing specific formatting behavior (substitutions, compound strings), add locale-specific assertions: ```swift @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. --- ## LocalizableTestCase Protocol Test suites conform to `LocalizableTestCase` to access testing infrastructure: ```swift 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). ### What LocalizableTestCase Provides | 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 | ### Testing Methods | 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 | --- ## YAML Requirements ### ViewModels with @LocalizedString Every ViewModel with `@LocalizedString` properties needs YAML entries: ```swift @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 } ``` ```yaml # DashboardViewModel.yml en: DashboardViewModel: pageTitle: "Dashboard" emptyMessage: "No items yet" es: DashboardViewModel: pageTitle: "Tablero" emptyMessage: "No hay elementos todavía" ``` ### Embedded ViewModels When a ViewModel contains child ViewModels, all types in the hierarchy need YAML entries: ```swift @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). ### Private Test ViewModels When tests define private ViewModel structs for testing specific scenarios, those also need YAML: ```swift // 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. --- ## How to Use This Skill **Invocation:** /fosmvvm-viewmodel-test-generator **Prerequisites:** - ViewModel structure understood from conversation context - Localization properties identified (@LocalizedString, @LocalizedSubs, etc.) - YAML localization files exist or will be created - Child ViewModels identified (if any) **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. ## Pattern Implementation This skill references conversation context to determine test structure: ### ViewModel Analysis From conversation context, the skill identifies: - **ViewModels to test** (from prior discussion or codebase) - **Localization requirements** (@LocalizedString properties) - **Child ViewModels** (embedded within parent) - **Substitution behavior** (@LocalizedSubs needing specific verification) ### YAML Coverage Check Verifies completeness: - **ViewModel YAML entries** (all @LocalizedString properties) - **Child ViewModel entries** (nested types) - **Locale coverage** (en, es, or project-specific locales) ### Test File Generation Creates test suite with: - LocalizableTestCase conformance - Localization store initialization - expectFullViewModelTests() calls for each ViewModel - Optional specific formatting tests (substitutions, compound strings) ### Context Sources Skill references information from: - **Prior conversation**: ViewModels discussed or recently created - **ViewModel code**: If Claude has read ViewModel files into context - **YAML files**: From codebase analysis of existing localizations - **Test patterns**: From existing test files in project --- ## File Templates See [reference.md](reference.md) for complete file templates. --- ## Common Scenarios ### Testing a Single Top-Level ViewModel ```swift @Test func dashboardViewModel() throws { try expectFullViewModelTests(DashboardViewModel.self) } ``` ### Testing Multiple Related ViewModels ```swift @Test func boardViewModels() throws { try expectFullViewModelTests(BoardViewModel.self) try expectFullViewModelTests(ColumnViewModel.self) try expectFullViewModelTests(CardViewModel.self) } ``` ### Testing with Custom Locales ```swift var locales: Set<Locale> { [en, es, enGB] } // Override default @Test func multiLocaleViewModel() throws { try expectFullViewModelTests(MyViewModel.self) // Tests en, es, AND en-GB } ``` ### Testing Substitution Behavior ```swift @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!") } ``` ### Testing Embedded ViewModels ```swift @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") } ``` --- ## Troubleshooting ### "Missing Translation" Error ``` FOSLocalizableError: _pageTitle -- Missing Translation -- en ``` **Cause:** YAML entry missing for a `@LocalizedString` property. **Fix:** Add the property to the YAML file: ```yaml en: MyViewModel: pageTitle: "Page Title" # Add this ``` ### "Is pending localization" Error **Cause:** The ViewModel wasn't encoded with a localizing encoder. **Fix:** Ensure using `encoder(locale:)` or `expectFullViewModelTests()`. ### Test Passes But Translations Seem Wrong **Cause:** YAML values exist but may have typos or wrong content. **Fix:** Add specific assertions to verify exact values: ```swift let vm = try .stub().toJSON(encoder: encoder(locale: en)).fromJSON() #expect(try vm.title.localizedString == "Expected Value") ``` --- ## Naming Conventions | 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()` | --- ## See Also - [FOSMVVMArchitecture.md - Testing Support](../../docs/FOSMVVMArchitecture.md#testing-support) - Architecture overview - [fosmvvm-viewmodel-generator](../fosmvvm-viewmodel-generator/SKILL.md) - For creating ViewModels - [fosmvvm-fields-generator](../fosmvvm-fields-generator/SKILL.md) - For form validation testing - [reference.md](reference.md) - Complete file templates --- ## Version History | 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. |

path: reference.md
content: # FOSMVVM ViewModel Test Generator - Reference Templates

Complete file templates for generating ViewModel tests.

> **Conceptual context:** See [SKILL.md](SKILL.md) for when and why to use this skill.
> **Architecture context:** See [FOSMVVMArchitecture.md](../../docs/FOSMVVMArchitecture.md#testing-support) for testing overview.

## Placeholders

| Placeholder | Replace With | Example |
|-------------|--------------|---------|
| `{Name}` | Feature or ViewModel name (PascalCase) | `Dashboard`, `Board` |
| `{Target}` | Your test target name | `FOSMVVM`, `ViewModels` |
| `{ResourceDir}` | YAML resource directory | `TestYAML`, `Resources` |

---

# Test File Templates

---

## Template 1: Basic ViewModel Test Suite

For testing one or more ViewModels with standard coverage.

**Location:** `Tests/{Target}Tests/Localization/{Name}ViewModelTests.swift`

```swift
// {Name}ViewModelTests.swift
//
// Copyright 2025 {Your Company}
//
// Licensed under the Apache License, Version 2.0 (the  License);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import FOSFoundation
@testable import FOSMVVM
import FOSTesting
import Foundation
import Testing

@Suite("{Name} ViewModel Tests")
struct {Name}ViewModelTests: LocalizableTestCase {
    @Test func {name}ViewModel() throws {
        try expectFullViewModelTests({Name}ViewModel.self)
    }

    let locStore: LocalizationStore
    init() throws {
        self.locStore = try Self.loadLocalizationStore(
            bundle: Bundle.module,
            resourceDirectoryName: "{ResourceDir}"
        )
    }
}
```

---

## Template 2: Test Suite with Multiple ViewModels

For testing a feature area with multiple related ViewModels.

**Location:** `Tests/{Target}Tests/Localization/{Name}ViewModelTests.swift`

```swift
// {Name}ViewModelTests.swift

import FOSFoundation
@testable import FOSMVVM
import FOSTesting
import Foundation
import Testing

@Suite("{Name} ViewModel Tests")
struct {Name}ViewModelTests: LocalizableTestCase {
    // MARK: - Top-Level ViewModels

    @Test func {name}ViewModel() throws {
        try expectFullViewModelTests({Name}ViewModel.self)
    }

    // MARK: - Child ViewModels

    @Test func {childName}ViewModel() throws {
        try expectFullViewModelTests({ChildName}ViewModel.self)
    }

    @Test func {otherChildName}ViewModel() throws {
        try expectFullViewModelTests({OtherChildName}ViewModel.self)
    }

    // MARK: - Setup

    let locStore: LocalizationStore
    init() throws {
        self.locStore = try Self.loadLocalizationStore(
            bundle: Bundle.module,
            resourceDirectoryName: "{ResourceDir}"
        )
    }
}
```

---

## Template 3: Test Suite with Specific Behavior Verification

For testing ViewModels that have substitutions or specific formatting to verify.

**Location:** `Tests/{Target}Tests/Localization/{Name}ViewModelTests.swift`

```swift
// {Name}ViewModelTests.swift

import FOSFoundation
@testable import FOSMVVM
import FOSTesting
import Foundation
import Testing

@Suite("{Name} ViewModel Tests")
struct {Name}ViewModelTests: LocalizableTestCase {
    @Test func {name}ViewModel() throws {
        try expectFullViewModelTests({Name}ViewModel.self)
    }

    @Test func {name}Substitutions() throws {
        try expectFullViewModelTests({Name}ViewModel.self)

        // Verify specific substitution behavior
        let vm: {Name}ViewModel = try .stub()
            .toJSON(encoder: encoder(locale: en))
            .fromJSON()

        #expect(try vm.welcomeMessage.localizedString == "Welcome, {ExpectedValue}!")
        #expect(try vm.itemCount.localizedString == "{ExpectedCount} items")
    }

    let locStore: LocalizationStore
    init() throws {
        self.locStore = try Self.loadLocalizationStore(
            bundle: Bundle.module,
            resourceDirectoryName: "{ResourceDir}"
        )
    }
}
```

---

## Template 4: Test Suite with Embedded ViewModels

For testing parent ViewModels that contain child ViewModels.

**Location:** `Tests/{Target}Tests/Localization/{Name}ViewModelTests.swift`

```swift
// {Name}ViewModelTests.swift

import FOSFoundation
@testable import FOSMVVM
import FOSTesting
import Foundation
import Testing

@Suite("{Name} Embedded ViewModel Tests")
struct {Name}ViewModelTests: LocalizableTestCase {
    @Test func parentWithEmbeddedChildren() throws {
        try expectFullViewModelTests({Parent}ViewModel.self)
    }

    @Test func embeddedChildValues() throws {
        try expectFullViewModelTests({Parent}ViewModel.self)

        // Verify embedded child ViewModels have correct values
        let vm: {Parent}ViewModel = try .stub()
            .toJSON(encoder: encoder(locale: en))
            .fromJSON()

        #expect(try vm.children[0].title.localizedString == "Child Title")
        #expect(try vm.children[1].title.localizedString == "Child Title")
    }

    @Test func multipleChildrenOfSameType() throws {
        try expectFullViewModelTests({Parent}ViewModel.self)

        let vm: {Parent}ViewModel = try .stub()
            .toJSON(encoder: encoder(locale: en))
            .fromJSON()

        // Verify each child has its own substitution values (not shared)
        #expect(try vm.child1.value.localizedString == "Value: 1")
        #expect(try vm.child2.value.localizedString == "Value: 2")
    }

    let locStore: LocalizationStore
    init() throws {
        self.locStore = try Self.loadLocalizationStore(
            bundle: Bundle.module,
            resourceDirectoryName: "{ResourceDir}"
        )
    }
}
```

---

## Template 5: Test Suite with Private Test ViewModels

For testing specific scenarios using private ViewModel structs defined in the test file.

**Location:** `Tests/{Target}Tests/Localization/{Name}Tests.swift`

```swift
// {Name}Tests.swift

import FOSFoundation
@testable import FOSMVVM
import FOSTesting
import Foundation
import Testing

@Suite("{Name} Tests")
struct {Name}Tests: LocalizableTestCase {
    @Test func {scenarioName}() throws {
        try expectFullViewModelTests(Test{Name}ViewModel.self)
    }

    @Test func {anotherScenario}() throws {
        try expectFullViewModelTests(TestParentViewModel.self)

        let vm: TestParentViewModel = try .stub()
            .toJSON(encoder: encoder(locale: en))
            .fromJSON()

        #expect(try vm.child.label.localizedString == "Expected Label")
    }

    let locStore: LocalizationStore
    init() throws {
        self.locStore = try Self.loadLocalizationStore(
            bundle: Bundle.module,
            resourceDirectoryName: "{ResourceDir}"
        )
    }
}

// MARK: - Private Test ViewModels

private struct Test{Name}ViewModel: ViewModel {
    @LocalizedString var title

    var vmId: ViewModelId = .init()

    static func stub() -> Self {
        .init()
    }
}

private struct TestParentViewModel: ViewModel {
    @LocalizedString var parentTitle
    let child: TestChildViewModel

    var vmId: ViewModelId = .init()

    static func stub() -> Self {
        .init(child: .stub())
    }
}

private struct TestChildViewModel: ViewModel {
    @LocalizedString var label

    var vmId: ViewModelId = .init()

    static func stub() -> Self {
        .init()
    }
}
```

---

## Template 6: Test Suite with Custom Locales

For testing with additional locales beyond the default (en, es).

**Location:** `Tests/{Target}Tests/Localization/{Name}ViewModelTests.swift`

```swift
// {Name}ViewModelTests.swift

import FOSFoundation
@testable import FOSMVVM
import FOSTesting
import Foundation
import Testing

@Suite("{Name} ViewModel Tests")
struct {Name}ViewModelTests: LocalizableTestCase {
    // Override default locales
    var locales: Set<Locale> { [en, es, enGB, enUS] }

    @Test func {name}ViewModel() throws {
        // Tests all four locales
        try expectFullViewModelTests({Name}ViewModel.self)
    }

    @Test func {name}SpecificLocale() throws {
        // Test specific locale behavior
        let vmGB: {Name}ViewModel = try .stub()
            .toJSON(encoder: encoder(locale: enGB))
            .fromJSON()

        #expect(try vmGB.colorLabel.localizedString == "Colour")  // British spelling
    }

    let locStore: LocalizationStore
    init() throws {
        self.locStore = try Self.loadLocalizationStore(
            bundle: Bundle.module,
            resourceDirectoryName: "{ResourceDir}"
        )
    }
}
```

---

# YAML Templates

---

## Template 7: Basic ViewModel YAML

**Location:** `Tests/{Target}Tests/{ResourceDir}/{Name}ViewModel.yml`

```yaml
en:
  {Name}ViewModel:
    pageTitle: "Page Title"
    subtitle: "Subtitle text"
    emptyMessage: "No items found"
    buttonLabel: "Submit"

es:
  {Name}ViewModel:
    pageTitle: "Título de Página"
    subtitle: "Texto del subtítulo"
    emptyMessage: "No se encontraron elementos"
    buttonLabel: "Enviar"
```

---

## Template 8: ViewModel with Substitutions YAML

**Location:** `Tests/{Target}Tests/{ResourceDir}/{Name}ViewModel.yml`

```yaml
en:
  {Name}ViewModel:
    welcomeMessage: "Welcome, %{userName}!"
    itemCount: "%{count} items"
    lastUpdated: "Last updated: %{date}"

es:
  {Name}ViewModel:
    welcomeMessage: "Bienvenido, %{userName}!"
    itemCount: "%{count} elementos"
    lastUpdated: "Última actualización: %{date}"
```

---

## Template 9: Multiple ViewModels in One YAML File

**Location:** `Tests/{Target}Tests/{ResourceDir}/{Feature}ViewModels.yml`

```yaml
en:
  ParentViewModel:
    title: "Parent Title"
    description: "Parent description"

  ChildViewModel:
    label: "Child Label"
    value: "Value: %{number}"

  AnotherChildViewModel:
    name: "Another Child"

es:
  ParentViewModel:
    title: "Título del Padre"
    description: "Descripción del padre"

  ChildViewModel:
    label: "Etiqueta del Hijo"
    value: "Valor: %{number}"

  AnotherChildViewModel:
    name: "Otro Hijo"
```

---

## Template 10: Test-Only Private ViewModels YAML

**Location:** `Tests/{Target}Tests/{ResourceDir}/{TestName}Models.yml`

```yaml
en:
  Test{Name}ViewModel:
    title: "Test Title"

  TestParentViewModel:
    parentTitle: "Parent"

  TestChildViewModel:
    label: "Child Label"

es:
  Test{Name}ViewModel:
    title: "Título de Prueba"

  TestParentViewModel:
    parentTitle: "Padre"

  TestChildViewModel:
    label: "Etiqueta del Hijo"
```

---

# Checklists

## New ViewModel Test Checklist

- [ ] Test file created in `Tests/{Target}Tests/Localization/`
- [ ] Test suite conforms to `LocalizableTestCase`
- [ ] `locStore` initialized from bundle
- [ ] `@Test` method calls `expectFullViewModelTests()`
- [ ] YAML file exists with entries for all locales
- [ ] All `@LocalizedString` properties have YAML entries
- [ ] Child ViewModels have YAML entries
- [ ] Tests pass: `swift test --filter {TestSuiteName}`

## Embedded ViewModel Test Checklist

- [ ] Parent ViewModel has YAML entries
- [ ] All child ViewModel types have YAML entries
- [ ] `expectFullViewModelTests()` called on parent
- [ ] Optional: Specific child value assertions added

## Substitution Test Checklist

- [ ] YAML has `%{key}` substitution placeholders
- [ ] ViewModel has `@LocalizedSubs` property
- [ ] `subs` dictionary maps keys to values
- [ ] Test verifies substituted result matches expected

---

## Quick Reference

**Minimal test:**
```swift
@Test func myViewModel() throws {
    try expectFullViewModelTests(MyViewModel.self)
}
```

**With behavior verification:**
```swift
@Test func myViewModelBehavior() throws {
    try expectFullViewModelTests(MyViewModel.self)

    let vm: MyViewModel = try .stub()
        .toJSON(encoder: encoder(locale: en))
        .fromJSON()

    #expect(try vm.property.localizedString == "Expected")
}
```

**Minimal YAML:**
```yaml
en:
  MyViewModel:
    property: "Value"

es:
  MyViewModel:
    property: "Valor"
```


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)