How do you ask AI technical questions about Moqui?

From time to time I am trying to use ChatGPT to ask technical questions about Moqui when I don’t know something, and each time it mostly spits out nonsense.

For example these are some of the latest questions I asked:

Me: how do i test that a service has thrown a certain error when i test a service in the Moqui framework?
ChatGPT response:
You can test it like this in a Moqui test class (Groovy):

def "should throw validation exception when paramA is missing"() {
        when:
        ec.service.runSync("example.test.throwErrorService", [:])

        then:
        def e = thrown(ServiceValidationException)
        e.message.contains("paramA is required")
    }

That it total garbage. First of all there is no runSync method. Secondly, if there is a validation error, the sync().call() does not throw an error, it returns an empty response. And you don’t get the error message from e.message, you get it from ec.message.getErrorsString().

Another example:

ChatGPT: You can load an XML document into an MNode like this:
MNode rootNode = MNodeSerializer.fromXmlText(xmlString)

There is no MNodeSerializer in Moqui. To parse xml you need to do MNode.parseText(null, xmlString)

So again, my question is: have you found a way to ask AI about Moqui without spitting out nonsense? Do you use a chat agent directly or do you use some tool inside the IDE?

i entered a basic question: explain the facade implementation in Moqui
the answer from (Claude sonnet 4,5)

Moqui Facade Implementation

The Facade Pattern is core to Moqui’s architecture. It provides simplified, high-level interfaces to complex subsystems while maintaining clean separation of concerns.


Architecture Overview

┌─────────────────────────────────────────────────────────┐
│         ExecutionContextFactory (Singleton)             │
│  - Manages framework lifecycle                          │
│  - Creates ExecutionContext instances                   │
│  - Holds shared facades (Entity, Service, Screen, etc.) │
└─────────────────────────────────────────────────────────┘
                        │ creates
                        ↓
┌─────────────────────────────────────────────────────────┐
│         ExecutionContext (Thread-local)                 │
│  - One per thread/request                               │
│  - Provides access to ALL facades                       │
│  - Manages context stack and lifecycle                  │
└─────────────────────────────────────────────────────────┘
                        │ provides access to
                        ↓
┌──────────────────────────────────────────────────────────────┐
│                      FACADE LAYER                            │
├──────────────────────────────────────────────────────────────┤
│ EntityFacade     │ ServiceFacade    │ UserFacade            │
│ TransactionFacade│ ScreenFacade     │ MessageFacade         │
│ LoggerFacade     │ CacheFacade      │ L10nFacade            │
│ ResourceFacade   │ WebFacade        │ ArtifactExecutionFacade│
└──────────────────────────────────────────────────────────────┘

Key Concepts

1. ExecutionContext - The Central Hub

The ExecutionContext (aliased as ec) is the single entry point to all Moqui capabilities:

ExecutionContext ec = context.ec ?: context

// Access to ALL facades through ec:
ec.entity      // Database operations
ec.service     // Service calls
ec.user        // User authentication & preferences
ec.transaction // Transaction management
ec.logger      // Logging
ec.message     // User messages/errors
ec.web         // HTTP request/response
ec.screen      // Screen rendering
ec.cache       // Caching
ec.l10n        // Localization
ec.resource    // Resource loading

Lifetime:

  • One per thread
  • Created at request start
  • Destroyed at request end
  • Automatically manages cleanup

2. Facade Interfaces - Simplified APIs

Each facade provides a clean, high-level API to a complex subsystem:

EntityFacade - Database Operations

// Find entity
def landingPage = ec.entity.find("growerp.landing.LandingPage")
    .condition("landingPageId", pageId)
    .one()

// Create entity
ec.entity.makeValue("growerp.landing.PageSection")
    .setAll([
        landingPageId: pageId,
        title: "Hero Section",
        description: "Welcome message"
    ])
    .create()

// Update
landingPage.set("isPublished", "Y")
landingPage.update()

ServiceFacade - Service Calls

// Synchronous service call
def result = ec.service.sync()
    .name("growerp.100.GeneralServices100.getNext#PseudoId")
    .parameters([
        ownerPartyId: ownerPartyId,
        entityName: "growerp.landing.LandingPage"
    ])
    .call()

// Async service call
ec.service.async()
    .name("send#EmailNotification")
    .parameters([email: userEmail])
    .call()

UserFacade - User Context

// Get current user info
String userId = ec.user.userId
String username = ec.user.username
EntityValue userAccount = ec.user.userAccount

// Preferences
String apiKey = ec.user.getPreference("GEMINI_API_KEY")

// Authentication
ec.user.loginUser("john.doe", "password")
ec.user.logoutUser()

// Timestamps
Timestamp now = ec.user.nowTimestamp
Calendar nowCal = ec.user.nowCalendar

MessageFacade - User Messages

// Add error message
ec.message.addError("Invalid input provided")

// Add validation error
ec.message.addValidationError(
    "productId", 
    "Product not found"
)

// Get all messages
List<String> errors = ec.message.getErrors()
List<String> messages = ec.message.getMessages()

LoggerFacade - Logging

ec.logger.info("Processing landing page generation")
ec.logger.warn("API call failed: ${statusCode}")
ec.logger.error("Unexpected error", exception)

TransactionFacade - Transaction Management

boolean beganTransaction = ec.transaction.begin(null)
try {
    // Do database operations
    ec.entity.makeValue("Order").create()
    ec.entity.makeValue("OrderItem").create()
    
    ec.transaction.commit(beganTransaction)
} catch (Exception e) {
    ec.transaction.rollback(beganTransaction, "Error occurred", e)
}

WebFacade - HTTP Operations

// Request parameters
String paramValue = ec.web.parameters.get("productId")

// Send response
ec.web.sendTextResponse("Success")
ec.web.sendJsonResponse([status: "ok", data: result])
ec.web.sendError(404, "Not found", null)

// Session
ec.web.sessionAttributes.put("cartId", orderId)
String cartId = ec.web.sessionAttributes.get("cartId")

// Headers
String hostname = ec.web.getHostName(false)
String clientIp = ec.user.getClientIp()

3. Two-Level Facade Architecture

Moqui uses a two-level facade pattern:

Level 1: ExecutionContextFactory (Singleton)

  • One instance per framework
  • Created at framework startup
  • Holds shared, thread-safe facades
  • Manages framework lifecycle
public interface ExecutionContextFactory {
    EntityFacade getEntity();
    ServiceFacade getService();
    ScreenFacade getScreen();
    // etc.
}

Level 2: ExecutionContext (Thread-local)

  • One instance per thread/request
  • References facades from factory
  • Adds thread-specific facades (User, Message, Web)
  • Manages request lifecycle
public class ExecutionContextImpl implements ExecutionContext {
    public final ExecutionContextFactoryImpl ecfi;
    
    // Thread-specific facades (created per request)
    public final UserFacadeImpl userFacade;
    public final MessageFacadeImpl messageFacade;
    public final ArtifactExecutionFacadeImpl artifactExecutionFacade;
    
    // Shared facades (from factory)
    public final CacheFacadeImpl cacheFacade;
    public final ServiceFacadeImpl serviceFacade;
    public final EntityFacadeImpl entityFacade;
}

4. Lifecycle Management

Framework Startup

// In Moqui.java
ExecutionContextFactory ecf = Moqui.getExecutionContextFactory();

Request Lifecycle

// 1. Request starts
ExecutionContext ec = ecf.getExecutionContext()

// 2. Initialize web facade (if HTTP request)
ec.initWebFacade(webappName, request, response)

// 3. Execute your code
def result = ec.service.sync().name("myService").call()

// 4. Request ends - cleanup
ec.destroy()  // Auto-commits transactions, cleans resources

Service Script Context

// In Groovy service scripts, ec is auto-available:
ExecutionContext ec = context.ec ?: context

// Now use facades
ec.logger.info("Service executing")

5. Benefits of Facade Pattern in Moqui

:white_check_mark: Simplified API

  • Complex subsystems hidden behind simple methods
  • Consistent interface across all functionality
  • Easy to learn and use

:white_check_mark: Dependency Injection

  • No need to manually create/manage objects
  • ec provides everything you need
  • Testable (can mock facades)

:white_check_mark: Thread Safety

  • Each thread gets its own ExecutionContext
  • Shared facades are thread-safe
  • No concurrency issues

:white_check_mark: Lifecycle Management

  • Auto-cleanup on request end
  • Transaction management handled
  • Resource pooling managed

:white_check_mark: Loose Coupling

  • Components depend on facade interfaces, not implementations
  • Easy to swap implementations
  • Better testability

6. Real Example from GrowERP

// From generateLandingPageWithAI.groovy

import org.moqui.context.ExecutionContext

// Get ExecutionContext
ExecutionContext ec = context.ec ?: context

// Use multiple facades through ec:

// 1. User facade - get API key preference
def apiKey = ec.user.getPreference("GEMINI_API_KEY")

// 2. Service facade - call another service
def ownerResult = ec.service.sync()
    .name("growerp.100.GeneralServices100.get#RelatedCompanyAndOwner")
    .call()

// 3. Service facade - generate pseudo ID
def pseudoIdResult = ec.service.sync()
    .name("growerp.100.GeneralServices100.getNext#PseudoId")
    .parameters([
        ownerPartyId: ownerPartyId,
        entityName: "growerp.landing.LandingPage"
    ])
    .call()

// 4. Service facade - create landing page
def createPageResult = ec.service.sync()
    .name("create#growerp.landing.LandingPage")
    .parameters(pageData)
    .call()

// 5. Message facade - handle errors
if (!ownerPartyId) {
    ec.message.addError("Unable to determine owner party ID")
    return
}

// 6. Logger facade - log progress
ec.logger.info("AI Landing Page Generation requested")

7. Common Patterns

Pattern 1: Service Call with Error Handling

ExecutionContext ec = context.ec ?: context

try {
    def result = ec.service.sync()
        .name("myService")
        .parameters([param1: value1])
        .call()
    
    if (ec.message.hasError()) {
        ec.logger.warn("Service returned errors: ${ec.message.getErrorsString()}")
        return
    }
    
    ec.logger.info("Service completed successfully")
} catch (Exception e) {
    ec.logger.error("Service failed", e)
    ec.message.addError("Service execution failed: ${e.message}")
}

Pattern 2: Database Transaction

boolean beganTransaction = ec.transaction.begin(600) // 10 min timeout
try {
    // Multiple database operations
    def order = ec.entity.makeValue("Order")
        .setAll([orderId: orderId, customerId: customerId])
        .create()
    
    items.each { item ->
        ec.entity.makeValue("OrderItem")
            .setAll([orderId: orderId, productId: item.productId])
            .create()
    }
    
    ec.transaction.commit(beganTransaction)
    ec.logger.info("Transaction committed successfully")
} catch (Exception e) {
    ec.transaction.rollback(beganTransaction, "Failed to create order", e)
    ec.message.addError("Order creation failed")
    throw e
}

Pattern 3: REST Client Call

import org.moqui.util.RestClient

ExecutionContext ec = context.ec ?: context

RestClient client = ec.service.rest()
    .method(RestClient.POST)
    .uri("https://api.external.com/endpoint")
    .addHeader("Authorization", "Bearer ${apiKey}")
    .addBodyParameters([data: payload])
    .connectTimeout(30)
    .readTimeout(90)

RestClient.RestResponse response = client.call()

if (response.statusCode >= 200 && response.statusCode < 300) {
    Map result = response.jsonObject()
    ec.logger.info("API call successful: ${result}")
} else {
    ec.logger.error("API call failed: ${response.statusCode}")
    ec.message.addError("External API returned error: ${response.statusCode}")
}

Summary

The Facade Pattern in Moqui provides:

  1. ExecutionContext (ec) as the central hub accessing all facades
  2. Each facade provides a simplified API to a complex subsystem
  3. Two-level architecture: Factory (singleton) + Context (thread-local)
  4. Automatic lifecycle management: Create → Use → Destroy
  5. Benefits: Simplicity, thread-safety, loose coupling, testability

Instead of managing dozens of classes, you just work with ec.entity, ec.service, ec.user, etc. - clean, simple, powerful! :bullseye:


Quick Reference

Facade Purpose Common Methods
ec.entity Database operations find(), makeValue(), fastFindOne()
ec.service Service calls sync(), async(), rest()
ec.user User context userId, username, getPreference(), loginUser()
ec.transaction Transactions begin(), commit(), rollback()
ec.logger Logging info(), warn(), error()
ec.message User messages addError(), addMessage(), getErrors()
ec.web HTTP operations parameters, sessionAttributes, sendResponse()
ec.cache Caching get(), put(), clear()
ec.l10n Localization localize(), format()
ec.resource Resources getLocationReference(), getLocationText()

See Also

Moqui Service Testing Guide

How to test that a service has thrown a certain error when testing services in the Moqui framework.


Key Concept: Moqui Error Handling Philosophy

In Moqui, services typically don’t throw exceptions for business logic errors. Instead, they add error messages to the MessageFacade (ec.message). This is a fundamental difference from traditional exception-based error handling.

Why This Matters

  • :white_check_mark: Allows collecting multiple errors in a single service call
  • :white_check_mark: Errors can be field-specific (validation errors)
  • :white_check_mark: Easier to display errors in UI context
  • :white_check_mark: Services can continue executing to gather all validation issues

Quick Answer

Instead of using thrown(), check ec.message.hasError():

// ❌ DON'T do this (won't work for most Moqui services)
then:
thrown(ServiceException)

// ✅ DO this instead
then:
ec.message.hasError()
ec.message.errorsString.contains("expected error text")

Testing Methods

Method 1: Basic Error Detection (Most Common)

Test that a service adds an error message:

import spock.lang.*
import org.moqui.context.ExecutionContext
import org.moqui.Moqui

class MyServiceTests extends Specification {
    @Shared
    ExecutionContext ec

    def setupSpec() {
        ec = Moqui.getExecutionContext()
    }

    def cleanupSpec() {
        ec.destroy()
    }

    def "service should add error for invalid input"() {
        when:
        ec.service.sync()
            .name("growerp.100.MyService.create#Something")
            .parameters([invalidParam: "bad value"])
            .call()

        then:
        ec.message.hasError()

        cleanup:
        ec.message.clearErrors()
    }
}

Method 2: Test Specific Error Message

Verify the exact error message text:

def "service should return specific error message"() {
    when:
    ec.service.sync()
        .name("myService")
        .parameters([email: null])  // Missing required field
        .call()

    then:
    ec.message.hasError()
    ec.message.errors.contains("Email is required")
    
    // Alternative: check if error contains substring
    ec.message.errorsString.contains("required")

    cleanup:
    ec.message.clearErrors()
}

Method 3: Test Multiple Errors

Check that multiple validation errors are added:

def "service should add multiple validation errors"() {
    when:
    ec.service.sync()
        .name("create#User")
        .parameters([
            email: "invalid-email",
            password: "123",  // Too short
            age: -5  // Invalid
        ])
        .call()

    then:
    ec.message.hasError()
    ec.message.errors.size() >= 3
    ec.message.errorsString.contains("email")
    ec.message.errorsString.contains("password")
    ec.message.errorsString.contains("age")

    cleanup:
    ec.message.clearErrors()
}

Method 4: Test Field-Specific Validation Errors

Test validation errors that are tied to specific fields:

def "service should add validation error for invalid email field"() {
    when:
    ec.service.sync()
        .name("growerp.100.PartyServices100.register#User")
        .parameters([
            email: "not-an-email",
            firstName: "Test"
        ])
        .call()

    then:
    ec.message.hasError()
    ec.message.validationErrors.size() > 0
    
    // Check specific field
    def emailError = ec.message.validationErrors.find { it.field == "email" }
    emailError != null
    emailError.message.contains("invalid") || emailError.message.contains("format")

    cleanup:
    ec.message.clearErrors()
}

Method 5: Test Exception Throwing (Rare Cases)

For the rare cases where a service actually throws an exception:

import org.moqui.service.ServiceException

def "service should throw exception for critical system error"() {
    when:
    ec.service.sync()
        .name("service.that.throws")
        .parameters([param: "value"])
        .call()

    then:
    thrown(ServiceException)
}

def "service should throw specific exception with message"() {
    when:
    ec.service.sync()
        .name("service.that.throws")
        .call()

    then:
    ServiceException ex = thrown()
    ex.message.contains("Database connection failed")
}

Method 6: Comprehensive Error Testing

Test both error conditions and success conditions:

def "service should handle both valid and invalid inputs"() {
    when: "invalid input provided"
    def invalidResult = ec.service.sync()
        .name("process#Order")
        .parameters([orderId: "INVALID"])
        .call()

    then: "errors are added"
    ec.message.hasError()
    ec.message.errorsString.contains("Order not found")
    invalidResult.success == false

    when: "valid input provided"
    ec.message.clearErrors()
    def validResult = ec.service.sync()
        .name("process#Order")
        .parameters([orderId: "VALID_ORDER"])
        .call()

    then: "no errors"
    !ec.message.hasError()
    validResult.success == true
}

MessageFacade API Reference

Checking for Errors

// Check if any error exists
boolean hasError = ec.message.hasError()

// Get count of errors
int errorCount = ec.message.errors.size()

// Check specific error exists
boolean hasSpecific = ec.message.errors.contains("Exact error message")

// Check error substring
boolean hasSubstring = ec.message.errorsString.contains("partial error")

Getting Error Details

// Get all error messages as List
List<String> errorList = ec.message.errors

// Get all errors as concatenated String
String allErrors = ec.message.errorsString

// Get validation errors (field-specific)
List<ValidationError> validationErrors = ec.message.validationErrors

// Get specific validation error
def fieldError = ec.message.validationErrors.find { it.field == "email" }

Clearing Errors

// Clear only errors
ec.message.clearErrors()

// Clear all messages (errors, warnings, info)
ec.message.clearAll()

ValidationError Properties

ValidationError ve = ec.message.validationErrors[0]

String form = ve.form           // Form name (if applicable)
String field = ve.field         // Field name
String service = ve.serviceName // Service that added error
String message = ve.message     // Error message

Complete Test Class Example

import spock.lang.*
import org.moqui.context.ExecutionContext
import org.moqui.Moqui

class LandingPageServiceTests extends Specification {
    @Shared
    ExecutionContext ec

    def setupSpec() {
        // Initialize Moqui framework
        ec = Moqui.getExecutionContext()
        ec.user.loginUser("john.doe", "moqui")
    }

    def cleanupSpec() {
        // Cleanup framework
        ec.destroy()
    }

    def setup() {
        // Before each test
        ec.artifactExecution.disableAuthz()  // Disable authorization
        ec.message.clearAll()                 // Clear all messages
    }

    def cleanup() {
        // After each test
        ec.artifactExecution.enableAuthz()   // Re-enable authorization
        ec.message.clearAll()                 // Clear messages
    }

    def "test landing page generation without API key"() {
        given:
        ec.user.setPreference("GEMINI_API_KEY", null)

        when:
        ec.service.sync()
            .name("growerp.100.LandingPageServices100.generate#LandingPageWithAI")
            .parameters([
                businessDescription: "Test business",
                targetAudience: "Test audience"
            ])
            .call()

        then:
        ec.message.hasError()
        ec.message.errorsString.contains("Gemini API key not found")
        
        cleanup:
        ec.user.setPreference("GEMINI_API_KEY", "test-key")
    }

    def "test landing page creation with missing required field"() {
        when:
        ec.service.sync()
            .name("create#growerp.landing.LandingPage")
            .parameters([
                title: "Test Page"  // landingPageId is required but missing
            ])
            .call()

        then:
        ec.message.hasError()
        ec.message.validationErrors.size() > 0
    }

    def "test successful landing page creation has no errors"() {
        when:
        def result = ec.service.sync()
            .name("create#growerp.landing.LandingPage")
            .parameters([
                landingPageId: "TEST123",
                title: "Test Page",
                ownerPartyId: "DEMO_OWNER"
            ])
            .call()

        then:
        !ec.message.hasError()
        result.landingPageId == "TEST123"

        cleanup:
        ec.service.sync()
            .name("delete#growerp.landing.LandingPage")
            .parameters([landingPageId: "TEST123"])
            .call()
    }

    def "test service with multiple validation errors"() {
        when:
        ec.service.sync()
            .name("growerp.100.PartyServices100.register#User")
            .parameters([
                email: "invalid",
                firstName: "",  // Empty
                password: "123" // Too short
            ])
            .call()

        then:
        ec.message.hasError()
        ec.message.errors.size() >= 2
        ec.message.validationErrors.any { it.field == "email" }
        ec.message.errorsString.contains("email")
    }
}

Testing Best Practices

1. Always Clear Messages Between Tests

def setup() {
    ec.message.clearAll()  // Before each test
}

def cleanup() {
    ec.message.clearAll()  // After each test
}

Why? Messages persist in the ExecutionContext and can leak between tests.

2. Disable Authorization in Tests

def setup() {
    ec.artifactExecution.disableAuthz()
}

def cleanup() {
    ec.artifactExecution.enableAuthz()
}

Why? Tests should focus on business logic, not authorization.

3. Test Both Success and Error Cases

def "test service validation"() {
    when: "invalid input"
    // ... test error case
    
    when: "valid input"
    ec.message.clearErrors()
    // ... test success case
}

Why? Ensures service works correctly in both scenarios.

4. Use Specific Error Message Assertions

// ❌ Too generic
then:
ec.message.hasError()

// ✅ More specific
then:
ec.message.errorsString.contains("Email is required")
ec.message.validationErrors.any { it.field == "email" }

Why? Catches more bugs and makes tests self-documenting.

5. Clean Up Test Data

cleanup:
// Delete any test data created
ec.service.sync()
    .name("delete#TestEntity")
    .parameters([testId: "TEST123"])
    .call()

Why? Prevents test data accumulation and ensures clean state.

6. Use @Shared for ExecutionContext

@Shared
ExecutionContext ec

Why? Reuses the same ExecutionContext across all tests for better performance.


Common Testing Patterns

Pattern 1: Test Required Field Validation

def "test required field validation"() {
    when:
    ec.service.sync()
        .name("create#Entity")
        .parameters([
            // requiredField is missing
            optionalField: "value"
        ])
        .call()

    then:
    ec.message.hasError()
    ec.message.validationErrors.any { 
        it.field == "requiredField" && it.message.contains("required") 
    }
}

Pattern 2: Test Business Rule Validation

def "test age must be 18 or older"() {
    when:
    ec.service.sync()
        .name("register#User")
        .parameters([age: 17])
        .call()

    then:
    ec.message.hasError()
    ec.message.errorsString.contains("18 years") || 
        ec.message.errorsString.contains("age")
}

Pattern 3: Test Service Error vs Exception

def "test service handles database errors gracefully"() {
    when:
    // Simulate database error (duplicate key, etc.)
    ec.service.sync()
        .name("create#User")
        .parameters([userId: "EXISTING_ID"])
        .call()

    then:
    // Check if service added error instead of throwing
    ec.message.hasError()
    ec.message.errorsString.contains("already exists") ||
        ec.message.errorsString.contains("duplicate")
}

Pattern 4: Test Error Recovery

def "test service can recover after error"() {
    when: "first call fails"
    ec.service.sync().name("myService")
        .parameters([invalid: true])
        .call()

    then:
    ec.message.hasError()

    when: "clear errors and try again with valid data"
    ec.message.clearErrors()
    def result = ec.service.sync().name("myService")
        .parameters([valid: true])
        .call()

    then:
    !ec.message.hasError()
    result.success == true
}

Troubleshooting

Problem: Errors Persist Between Tests

Symptom:

// First test adds error
// Second test fails because hasError() is still true

Solution:

def cleanup() {
    ec.message.clearAll()  // or clearErrors()
}

Problem: Service Doesn’t Add Expected Error

Check:

  1. Service might be throwing exception instead (catch it)
  2. Error might be logged but not added to MessageFacade
  3. Service might be returning error in result Map
// Check all possibilities
then:
// Check MessageFacade
ec.message.hasError() ||
// Check result Map
result.error != null ||
// Check if exception was thrown
thrown(Exception)

Problem: Can’t Find Specific Validation Error

Debug:

then:
// Print all errors for debugging
println "Errors: ${ec.message.errors}"
println "Validation Errors: ${ec.message.validationErrors}"
ec.message.validationErrors.each { ve ->
    println "Field: ${ve.field}, Message: ${ve.message}"
}
ec.message.hasError()

Quick Reference

Error Checking Methods

Method Returns Use Case
ec.message.hasError() boolean Check if any error exists
ec.message.errors List<String> Get all error messages
ec.message.errorsString String Get concatenated error text
ec.message.validationErrors List<ValidationError> Get field-specific errors

Assertions Examples

// Check error exists
ec.message.hasError()

// Check error count
ec.message.errors.size() == 2

// Check specific error message
ec.message.errors.contains("Email is required")

// Check error contains substring
ec.message.errorsString.contains("invalid")

// Check validation error for field
ec.message.validationErrors.any { it.field == "email" }

// Check no errors
!ec.message.hasError()

Cleanup Methods

ec.message.clearErrors()      // Clear errors only
ec.message.clearAll()          // Clear all messages
ec.message.clearMessages()     // Clear non-error messages

Summary

Key Takeaways:

  1. :white_check_mark: Moqui services use ec.message for errors, not exceptions
  2. :white_check_mark: Always check ec.message.hasError() after service calls
  3. :white_check_mark: Clear messages between tests to prevent leakage
  4. :white_check_mark: Test specific error messages for better coverage
  5. :white_check_mark: Use thrown() only for exception-throwing services (rare)

Testing Formula:

when:
// Call service that should fail
ec.service.sync().name("serviceName").parameters([...]).call()

then:
// Check for error
ec.message.hasError()
ec.message.errorsString.contains("expected error text")

cleanup:
// Always clean up
ec.message.clearErrors()

See Also