-
-
Notifications
You must be signed in to change notification settings - Fork 131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Provide concurrency-safe setter methods for function fields #231
Comments
Hi @sapuri I am using |
@breml Thank you for your response! Below is an example illustrating why we need to reassign the mock function concurrently and the data race issue that arises. Although the example looks somewhat contrived, similar data races can realistically occur in scenarios where a workflow engine (like Temporal) runs in the background. In such systems, the mock’s method might be called by one goroutine while another goroutine updates the mock’s behavior to simulate changes during runtime. // myinterface.go
package example
type MyInterface interface {
One() bool
}
// Suppose moq generates something like:
//
// type MyInterfaceMock struct {
// OneFunc func() bool
// }
//
// In the current implementation, there's no concurrency-safe way
// to reassign OneFunc during test execution.
// myinterface_test.go
package example_test
import (
"sync"
"testing"
"time"
"example"
)
func TestConcurrentMockReassignment(t *testing.T) {
// created by moq
mock := &example.MyInterfaceMock{
OneFunc: func() bool { return false },
}
// Goroutine A: continuously calls mock.OneFunc()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100000; i++ {
_ = mock.OneFunc()
time.Sleep(time.Microsecond)
}
}()
// Goroutine B (main test routine): reassigns OneFunc repeatedly
// during the same time period as Goroutine A
for i := 0; i < 100000; i++ {
mock.OneFunc = func() bool { return true }
mock.OneFunc = func() bool { return false }
}
wg.Wait()
} As shown, modifying |
@sapuri Thanks for the example code reproducing the issue. It is absolutely clear, that this code will produce the data race. What is still unclear to me is, what kind of code do you test using In regards to changing the assignment of As mentioned above, I kind of doubt, that your issue is the way |
The examples I provided above for the queue are not concurrency safe, but this would be something, that can be easily added using a mutex. |
@breml Here’s a minimal example that demonstrates my use case:
This is conceptually similar to scenarios where a workflow engine like Temporal or custom orchestration logic runs asynchronously, calling a shared mock clock. // mock_clock.go
package example
import "time"
type MockClock struct {
NowFunc func() time.Time
}
func (c *MockClock) Now() time.Time {
return c.NowFunc()
}
// container.go
package example
type Container struct {
Clock *MockClock
WorkflowManager *WorkflowManager
}
// interactor.go
package example
import (
"context"
"time"
)
// Interactor represents business logic that will trigger workflows.
type Interactor struct {
c *Container
}
func NewInteractor(c *Container) *Interactor {
return &Interactor{c: c}
}
func (i *Interactor) CreateSomething(ctx context.Context) error {
return i.c.WorkflowManager.RunWorkflowA(ctx, i.c.Clock)
}
func (i *Interactor) CaptureSomething(ctx context.Context) error {
return i.c.WorkflowManager.RunWorkflowB(ctx, i.c.Clock)
}
// workflow.go
package example
import (
"context"
"fmt"
"time"
)
// WorkflowManager simulates a workflow engine that calls Clock.Now() in separate goroutines.
type WorkflowManager struct{}
func (wm *WorkflowManager) RunWorkflowA(ctx context.Context, clock *FakeClock) error {
// Contains logic that asynchronously calls clock.Now()
}
func (wm *WorkflowManager) RunWorkflowB(ctx context.Context, clock *FakeClock) error {
// Contains logic that asynchronously calls clock.Now()
}
// example_test.go
package example_test
import (
"context"
"testing"
"time"
"example"
)
// Prepare is a test helper function that changes the clock between two steps
// (CreateSomething and CaptureSomething), while background workflows may
// be calling Clock.Now() concurrently.
func Prepare(t *testing.T, createAt, captureAt time.Time) {
t.Helper()
ctx := context.Background()
c := &example.Container{
Clock: &example.FakeClock{NowFunc: time.Now},
WorkflowManager: &example.WorkflowManager{},
}
interactor := example.NewInteractor(c)
// 1) Set clock to createAt
c.Clock.NowFunc = func() time.Time { return createAt }
// 2) CreateSomething starts workflow A, which calls c.Clock.Now() asynchronously
if err := interactor.CreateSomething(ctx); err != nil {
t.Fatal(err)
}
// 3) Switch the clock to captureAt while workflow A is still running in the background
c.Clock.NowFunc = func() time.Time { return captureAt }
// 4) CaptureSomething starts workflow B, also calling c.Clock.Now() asynchronously
if err := interactor.CaptureSomething(ctx); err != nil {
t.Fatal(err)
}
// At this point, if c.Clock.NowFunc was changed while other goroutines
// were reading it, a data race can happen.
} It may be difficult to understand the necessity since this is a very simplified example, but when two workflows each execute asynchronous activity functions, the order of mock function calls may not be deterministic. |
You mention, that workflow A and workflow B are async. So you have no guarantee, that one happens after the other. You also don't have a guarantee, that workflow A already called For all of this, no change is needed to |
There are cases where we want to dynamically change the behavior of a mock method during testing. By default, moq generates a field like
MyMethodFunc func(...)
for each interface method, and we can reassign the function pointer in our tests. However, this often leads to data races when multiple goroutines are running (especially in parallel tests).So, how about generating concurrency-safe setter methods (using RWMutex) for each generated function pointer? For example:
After generation, we could have something like:
With this approach, tests can safely change the mock function at runtime without causing data races.
The text was updated successfully, but these errors were encountered: