This is an automated email from the ASF dual-hosted git repository.
alexstocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git
The following commit(s) were added to refs/heads/develop by this push:
new d0eb5e67e chore(test): add unit tests for registry and proxy (#3126)
d0eb5e67e is described below
commit d0eb5e67e61a35d873673d4ec9af4be503e72f75
Author: Aether <[email protected]>
AuthorDate: Mon Dec 22 07:35:30 2025 +0800
chore(test): add unit tests for registry and proxy (#3126)
Signed-off-by: Aetherance <[email protected]>
Co-authored-by: Aetherance <[email protected]>
---
proxy/proxy_factory/invoker_test.go | 149 +++++++++++++++++++++++++++
proxy/proxy_factory/utils_test.go | 110 ++++++++++++++++++++
registry/base_configuration_listener_test.go | 79 ++++++++++++++
registry/options_test.go | 89 ++++++++++++++++
registry/service_instance_test.go | 132 ++++++++++++++++++++++++
5 files changed, 559 insertions(+)
diff --git a/proxy/proxy_factory/invoker_test.go
b/proxy/proxy_factory/invoker_test.go
new file mode 100644
index 000000000..11230b0c2
--- /dev/null
+++ b/proxy/proxy_factory/invoker_test.go
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package proxy_factory
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+ "strings"
+ "testing"
+)
+
+import (
+ "github.com/stretchr/testify/assert"
+)
+
+import (
+ "dubbo.apache.org/dubbo-go/v3/common"
+ "dubbo.apache.org/dubbo-go/v3/common/constant"
+ "dubbo.apache.org/dubbo-go/v3/protocol/base"
+ "dubbo.apache.org/dubbo-go/v3/protocol/invocation"
+)
+
+type ProxyInvokerService struct{}
+
+func (s *ProxyInvokerService) Hello(_ context.Context, name string) (string,
error) {
+ return "hello:" + name, nil
+}
+
+type PassThroughService struct{}
+
+func (s *PassThroughService) Service(method string, argTypes []string, args
[][]byte, attachments map[string]any) (any, error) {
+ body := ""
+ if len(args) > 0 {
+ body = string(args[0])
+ }
+ return fmt.Sprintf("%s|%s|%s", method, strings.Join(argTypes, ","),
body), nil
+}
+
+func registerService(t *testing.T, protocol, interfaceName string, svc
common.RPCService) {
+ t.Helper()
+ _, err := common.ServiceMap.Register(interfaceName, protocol, "", "",
svc)
+ assert.NoError(t, err)
+ t.Cleanup(func() {
+ _ = common.ServiceMap.UnRegister(interfaceName, protocol,
common.ServiceKey(interfaceName, "", ""))
+ })
+}
+
+func newURL(protocol, interfaceName string) *common.URL {
+ return common.NewURLWithOptions(
+ common.WithProtocol(protocol),
+ common.WithPath(interfaceName),
+ common.WithInterface(interfaceName),
+ common.WithParams(url.Values{constant.InterfaceKey:
{interfaceName}}),
+ )
+}
+
+func TestProxyInvoker_Invoke(t *testing.T) {
+ const (
+ protocol = "test-protocol"
+ interfaceName = "ProxyInvokerService"
+ )
+ registerService(t, protocol, interfaceName, &ProxyInvokerService{})
+ u := newURL(protocol, interfaceName)
+ invoker := &ProxyInvoker{BaseInvoker: *base.NewBaseInvoker(u)}
+
+ t.Run("invoke success", func(t *testing.T) {
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName("Hello"),
+ invocation.WithArguments([]any{"world"}),
+ invocation.WithAttachments(map[string]any{"trace":
"t1"}),
+ )
+ result := invoker.Invoke(context.Background(), inv)
+ assert.NoError(t, result.Error())
+ assert.Equal(t, "hello:world", result.Result())
+ assert.Equal(t, "t1", result.Attachments()["trace"])
+ })
+
+ t.Run("method not found", func(t *testing.T) {
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName("Missing"),
+ invocation.WithArguments([]any{}),
+ )
+ result := invoker.Invoke(context.Background(), inv)
+ assert.Error(t, result.Error())
+ })
+
+ t.Run("service not found", func(t *testing.T) {
+ absentURL := newURL(protocol, "UnknownService")
+ absentInvoker := &ProxyInvoker{BaseInvoker:
*base.NewBaseInvoker(absentURL)}
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName("Hello"),
+ invocation.WithArguments([]any{}),
+ )
+ result := absentInvoker.Invoke(context.Background(), inv)
+ assert.Error(t, result.Error())
+ })
+}
+
+func TestPassThroughProxyInvoker_Invoke(t *testing.T) {
+ const (
+ protocol = "pass-protocol"
+ interfaceName = "PassThroughService"
+ )
+ registerService(t, protocol, interfaceName, &PassThroughService{})
+ u := newURL(protocol, interfaceName)
+ invoker := &PassThroughProxyInvoker{
+ ProxyInvoker: &ProxyInvoker{BaseInvoker:
*base.NewBaseInvoker(u)},
+ }
+
+ t.Run("pass through success", func(t *testing.T) {
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName("RawMethod"),
+ invocation.WithArguments([]any{[]byte("payload")}),
+ invocation.WithAttachments(map[string]any{
+ constant.ParamsTypeKey: []string{"bytes"},
+ "trace": "abc",
+ }),
+ )
+ result := invoker.Invoke(context.Background(), inv)
+ assert.NoError(t, result.Error())
+ assert.Equal(t, "RawMethod|bytes|payload", result.Result())
+ assert.Equal(t, "abc", result.Attachments()["trace"])
+ })
+
+ t.Run("argument type mismatch", func(t *testing.T) {
+ inv := invocation.NewRPCInvocationWithOptions(
+ invocation.WithMethodName("RawMethod"),
+ invocation.WithArguments([]any{"not-bytes"}),
+ )
+ result := invoker.Invoke(context.Background(), inv)
+ assert.EqualError(t, result.Error(), "the param type is not
[]byte")
+ })
+}
diff --git a/proxy/proxy_factory/utils_test.go
b/proxy/proxy_factory/utils_test.go
new file mode 100644
index 000000000..4afd7fdc3
--- /dev/null
+++ b/proxy/proxy_factory/utils_test.go
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package proxy_factory
+
+import (
+ "errors"
+ "reflect"
+ "testing"
+)
+
+import (
+ "github.com/stretchr/testify/assert"
+)
+
+type callLocalMethodSample struct{}
+
+func (s *callLocalMethodSample) Sum(a, b int) int {
+ return a + b
+}
+
+func (s *callLocalMethodSample) PanicError() {
+ panic(errors.New("boom"))
+}
+
+func (s *callLocalMethodSample) PanicString() {
+ panic("boom str")
+}
+
+func (s *callLocalMethodSample) PanicUnknown() {
+ panic(123)
+}
+
+func TestCallLocalMethod(t *testing.T) {
+ sample := &callLocalMethodSample{}
+ cases := []struct {
+ name string
+ method string
+ in []reflect.Value
+ assertErr func(t *testing.T, err error)
+ assertOut func(t *testing.T, out []reflect.Value)
+ }{
+ {
+ name: "call success",
+ method: "Sum",
+ in: []reflect.Value{reflect.ValueOf(sample),
reflect.ValueOf(1), reflect.ValueOf(2)},
+ assertErr: func(t *testing.T, err error) {
+ assert.NoError(t, err)
+ },
+ assertOut: func(t *testing.T, out []reflect.Value) {
+ assert.Len(t, out, 1)
+ assert.Equal(t, 3, out[0].Interface())
+ },
+ },
+ {
+ name: "panic with error",
+ method: "PanicError",
+ in: []reflect.Value{reflect.ValueOf(sample)},
+ assertErr: func(t *testing.T, err error) {
+ assert.EqualError(t, err, "boom")
+ },
+ },
+ {
+ name: "panic with string",
+ method: "PanicString",
+ in: []reflect.Value{reflect.ValueOf(sample)},
+ assertErr: func(t *testing.T, err error) {
+ assert.EqualError(t, err, "boom str")
+ },
+ },
+ {
+ name: "panic with unknown type",
+ method: "PanicUnknown",
+ in: []reflect.Value{reflect.ValueOf(sample)},
+ assertErr: func(t *testing.T, err error) {
+ assert.EqualError(t, err, "invoke function
error, unknow exception: 123")
+ },
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ m, ok := reflect.TypeOf(sample).MethodByName(tt.method)
+ if !ok {
+ t.Fatalf("method %s not found", tt.method)
+ }
+ out, err := callLocalMethod(m, tt.in)
+ if tt.assertErr != nil {
+ tt.assertErr(t, err)
+ }
+ if tt.assertOut != nil {
+ tt.assertOut(t, out)
+ }
+ })
+ }
+}
diff --git a/registry/base_configuration_listener_test.go
b/registry/base_configuration_listener_test.go
new file mode 100644
index 000000000..c5e414b69
--- /dev/null
+++ b/registry/base_configuration_listener_test.go
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package registry
+
+import (
+ "net/url"
+ "testing"
+)
+
+import (
+ "github.com/stretchr/testify/assert"
+)
+
+import (
+ "dubbo.apache.org/dubbo-go/v3/common"
+ "dubbo.apache.org/dubbo-go/v3/common/constant"
+ "dubbo.apache.org/dubbo-go/v3/config_center"
+)
+
+type testConfigurator struct {
+ url *common.URL
+ configured bool
+}
+
+func (c *testConfigurator) GetUrl() *common.URL {
+ return c.url
+}
+
+func (c *testConfigurator) Configure(_ *common.URL) {
+ c.configured = true
+}
+
+func TestToConfigurators(t *testing.T) {
+ makeConfigurator := func(u *common.URL) config_center.Configurator {
+ return &testConfigurator{url: u}
+ }
+
+ assert.Nil(t, ToConfigurators(nil, makeConfigurator))
+
+ emptyProtocolURL :=
common.NewURLWithOptions(common.WithProtocol(constant.EmptyProtocol))
+ assert.Empty(t, ToConfigurators([]*common.URL{emptyProtocolURL},
makeConfigurator))
+
+ anyhostOnly := common.NewURLWithOptions(common.WithParams(url.Values{
+ constant.AnyhostKey: {"true"},
+ }))
+ assert.Nil(t, ToConfigurators([]*common.URL{anyhostOnly},
makeConfigurator))
+
+ validURL := common.NewURLWithOptions(common.WithProtocol("override"),
common.WithParams(url.Values{
+ constant.AnyhostKey: {"true"},
+ "timeout": {"2s"},
+ }))
+ configurators := ToConfigurators([]*common.URL{validURL},
makeConfigurator)
+ assert.Len(t, configurators, 1)
+ assert.Equal(t, validURL, configurators[0].GetUrl())
+}
+
+func TestBaseConfigurationListenerOverrideUrl(t *testing.T) {
+ cfg := &testConfigurator{}
+ bcl := &BaseConfigurationListener{configurators:
[]config_center.Configurator{cfg}}
+ target := common.NewURLWithOptions()
+
+ bcl.OverrideUrl(target)
+ assert.True(t, cfg.configured)
+}
diff --git a/registry/options_test.go b/registry/options_test.go
new file mode 100644
index 000000000..ece7f6a04
--- /dev/null
+++ b/registry/options_test.go
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package registry
+
+import (
+ "testing"
+ "time"
+)
+
+import (
+ "github.com/stretchr/testify/assert"
+)
+
+import (
+ "dubbo.apache.org/dubbo-go/v3/common/constant"
+)
+
+func TestNewOptionsRequireProtocol(t *testing.T) {
+ assert.Panics(t, func() {
+ NewOptions()
+ })
+}
+
+func TestNewOptionsWithHelpers(t *testing.T) {
+ tests := []struct {
+ name string
+ opts []Option
+ wantProtocol string
+ wantID string
+ wantTimeout string
+ wantAddress string
+ }{
+ {
+ name: "zookeeper default id",
+ opts: []Option{WithZookeeper()},
+ wantProtocol: constant.ZookeeperKey,
+ wantID: constant.ZookeeperKey,
+ },
+ {
+ name: "etcd with custom id",
+ opts: []Option{WithEtcdV3(),
WithID("custom-id")},
+ wantProtocol: constant.EtcdV3Key,
+ wantID: "custom-id",
+ },
+ {
+ name: "address overrides protocol",
+ opts:
[]Option{WithAddress("nacos://127.0.0.1:8848")},
+ wantProtocol: constant.NacosKey,
+ wantID: constant.NacosKey,
+ wantAddress: "nacos://127.0.0.1:8848",
+ },
+ {
+ name: "timeout option",
+ opts: []Option{WithZookeeper(), WithTimeout(3 *
time.Second)},
+ wantProtocol: constant.ZookeeperKey,
+ wantID: constant.ZookeeperKey,
+ wantTimeout: "3s",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ options := NewOptions(tt.opts...)
+ assert.Equal(t, tt.wantProtocol,
options.Registry.Protocol)
+ assert.Equal(t, tt.wantID, options.ID)
+ if tt.wantTimeout != "" {
+ assert.Equal(t, tt.wantTimeout,
options.Registry.Timeout)
+ }
+ if tt.wantAddress != "" {
+ assert.Equal(t, tt.wantAddress,
options.Registry.Address)
+ }
+ })
+ }
+}
diff --git a/registry/service_instance_test.go
b/registry/service_instance_test.go
new file mode 100644
index 000000000..2e42e74e2
--- /dev/null
+++ b/registry/service_instance_test.go
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package registry
+
+import (
+ "fmt"
+ "net/url"
+ "testing"
+)
+
+import (
+ "github.com/stretchr/testify/assert"
+)
+
+import (
+ "dubbo.apache.org/dubbo-go/v3/common"
+ "dubbo.apache.org/dubbo-go/v3/common/constant"
+ "dubbo.apache.org/dubbo-go/v3/metadata/info"
+)
+
+func TestDefaultServiceInstance_GetAddressAndWeight(t *testing.T) {
+ inst := &DefaultServiceInstance{Host: "127.0.0.1", Port: 20880}
+ assert.Equal(t, "127.0.0.1:20880", inst.GetAddress())
+
+ instNoPort := &DefaultServiceInstance{Host: "127.0.0.1"}
+ assert.Equal(t, "127.0.0.1", instNoPort.GetAddress())
+
+ instWithAddress := &DefaultServiceInstance{Address: "custom"}
+ assert.Equal(t, "custom", instWithAddress.GetAddress())
+
+ assert.EqualValues(t, constant.DefaultWeight,
(&DefaultServiceInstance{}).GetWeight())
+ assert.Equal(t, int64(50), (&DefaultServiceInstance{Weight:
50}).GetWeight())
+}
+
+func TestDefaultServiceInstance_ToURLsWithEndpoints(t *testing.T) {
+ serviceURL := common.NewURLWithOptions(
+ common.WithProtocol("tri"),
+ common.WithIp("127.0.0.1"),
+ common.WithPort("2000"),
+ common.WithPath("DemoService"),
+ common.WithInterface("DemoService"),
+ common.WithMethods([]string{"SayHello"}),
+ common.WithParams(url.Values{constant.WeightKey: {"0"}}),
+ )
+ serviceInfo := info.NewServiceInfoWithURL(serviceURL)
+
+ t.Run("pick matching endpoint", func(t *testing.T) {
+ instance := &DefaultServiceInstance{
+ Host: "127.0.0.1",
+ Port: 20880,
+ Metadata: map[string]string{
+ constant.ServiceInstanceEndpoints:
`[{"port":3000,"protocol":"tri"},{"port":3001,"protocol":"rest"}]`,
+ },
+ Tag: "gray",
+ }
+
+ urls := instance.ToURLs(serviceInfo)
+ assert.Len(t, urls, 1)
+ got := urls[0]
+ assert.Equal(t, "tri", got.Protocol)
+ assert.Equal(t, "127.0.0.1", got.Ip)
+ assert.Equal(t, "3000", got.Port)
+ assert.Equal(t, "DemoService", got.Service())
+ assert.Equal(t, "gray", got.GetParam(constant.Tagkey, ""))
+ assert.Equal(t, fmt.Sprint(constant.DefaultWeight),
got.GetParam(constant.WeightKey, ""))
+ })
+
+ t.Run("fallback without endpoints", func(t *testing.T) {
+ instance := &DefaultServiceInstance{
+ Host: "127.0.0.1",
+ Port: 20880,
+ Metadata: map[string]string{
+ constant.ServiceInstanceEndpoints:
"invalid-json",
+ },
+ }
+ urls := instance.ToURLs(serviceInfo)
+ assert.Len(t, urls, 1)
+ assert.Equal(t, "20880", urls[0].Port)
+ })
+}
+
+func TestDefaultServiceInstance_GetEndPointsAndCopy(t *testing.T) {
+ instance := &DefaultServiceInstance{
+ Host: "127.0.0.1",
+ Port: 20880,
+ Tag: "blue",
+ Enable: true,
+ Healthy: true,
+ Metadata: map[string]string{
+ constant.ServiceInstanceEndpoints:
`[{"port":20880,"protocol":"tri"}]`,
+ "custom": "value",
+ },
+ ServiceMetadata: info.NewMetadataInfo("app", ""),
+ }
+
+ endpoints := instance.GetEndPoints()
+ assert.Len(t, endpoints, 1)
+ assert.Equal(t, 20880, endpoints[0].Port)
+ assert.Equal(t, "tri", endpoints[0].Protocol)
+
+ copied := instance.Copy(&Endpoint{Port: 3001})
+ copiedInstance, ok := copied.(*DefaultServiceInstance)
+ if !ok {
+ t.Fatalf("expected *DefaultServiceInstance, got %T", copied)
+ }
+ assert.Equal(t, 3001, copiedInstance.Port)
+ assert.Equal(t, instance.GetAddress(), copiedInstance.ID)
+ assert.Equal(t, instance.Metadata, copiedInstance.Metadata)
+ assert.Equal(t, instance.Tag, copiedInstance.Tag)
+
+ broken := &DefaultServiceInstance{
+ Metadata: map[string]string{
+ constant.ServiceInstanceEndpoints: "{broken",
+ },
+ }
+ assert.Nil(t, broken.GetEndPoints())
+}