add metadata unit test (#2665)

diff --git a/global/application_config.go b/global/application_config.go
index dbb35a9..9778997 100644
--- a/global/application_config.go
+++ b/global/application_config.go
@@ -44,14 +44,15 @@
 	}
 
 	return &ApplicationConfig{
-		Organization: c.Organization,
-		Name:         c.Name,
-		Module:       c.Module,
-		Group:        c.Group,
-		Version:      c.Version,
-		Owner:        c.Owner,
-		Environment:  c.Environment,
-		MetadataType: c.MetadataType,
-		Tag:          c.Tag,
+		Organization:        c.Organization,
+		Name:                c.Name,
+		Module:              c.Module,
+		Group:               c.Group,
+		Version:             c.Version,
+		Owner:               c.Owner,
+		Environment:         c.Environment,
+		MetadataType:        c.MetadataType,
+		Tag:                 c.Tag,
+		MetadataServicePort: c.MetadataServicePort,
 	}
 }
diff --git a/metadata/client.go b/metadata/client.go
index 6b9120e..a3e7dd4 100644
--- a/metadata/client.go
+++ b/metadata/client.go
@@ -20,7 +20,6 @@
 import (
 	"context"
 	"encoding/json"
-	"time"
 )
 
 import (
@@ -37,49 +36,35 @@
 	"dubbo.apache.org/dubbo-go/v3/registry"
 )
 
-const metadataProxyDefaultTimeout = 5000
+const defaultTimeout = "5s" // s
 
-// GetMetadataFromMetadataReport test depends on dubbo protocol, if dubbo not dependent on config package, can move to metadata dir
 func GetMetadataFromMetadataReport(revision string, instance registry.ServiceInstance) (*info.MetadataInfo, error) {
 	report := GetMetadataReport()
+	if report == nil {
+		return nil, perrors.New("no metadata report instance found,please check ")
+	}
 	return report.GetAppMetadata(instance.GetServiceName(), revision)
 }
 
 func GetMetadataFromRpc(revision string, instance registry.ServiceInstance) (*info.MetadataInfo, error) {
-	service, destroy, err := createRpcClient(instance)
-	if err != nil {
-		return nil, err
+	params := getMetadataServiceUrlParams(instance.GetMetadata()[constant.MetadataServiceURLParamsPropertyName])
+	url := buildMetadataServiceURL(instance.GetServiceName(), instance.GetHost(), params)
+	url.SetParam(constant.TimeoutKey, defaultTimeout)
+	rpcService := &remoteMetadataService{}
+	invoker := extension.GetProtocol(constant.Dubbo).Refer(url)
+	if invoker == nil {
+		return nil, perrors.New("create invoker error, can not connect to the metadata report server: " + url.Ip + ":" + url.Port)
 	}
-	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(metadataProxyDefaultTimeout))
-	defer cancel()
-	defer destroy()
-	return service.GetMetadataInfo(ctx, revision)
+	proxy := extension.GetProxyFactory(constant.DefaultKey).GetProxy(invoker, url)
+	proxy.Implement(rpcService)
+	defer invoker.Destroy()
+	return rpcService.GetMetadataInfo(context.TODO(), revision)
 }
 
 type remoteMetadataService struct {
 	GetMetadataInfo func(context context.Context, revision string) (*info.MetadataInfo, error) `dubbo:"getMetadataInfo"`
 }
 
-func createRpcClient(instance registry.ServiceInstance) (*remoteMetadataService, func(), error) {
-	params := getMetadataServiceUrlParams(instance.GetMetadata()[constant.MetadataServiceURLParamsPropertyName])
-	url := buildMetadataServiceURL(instance.GetServiceName(), instance.GetHost(), params)
-	return createRpcClientByUrl(url)
-}
-
-func createRpcClientByUrl(url *common.URL) (*remoteMetadataService, func(), error) {
-	rpcService := &remoteMetadataService{}
-	invoker := extension.GetProtocol(constant.Dubbo).Refer(url)
-	if invoker == nil {
-		return nil, nil, perrors.New("create invoker error, can not connect to the metadata report server: " + url.Ip + ":" + url.Port)
-	}
-	proxy := extension.GetProxyFactory(constant.DefaultKey).GetProxy(invoker, url)
-	proxy.Implement(rpcService)
-	destroy := func() {
-		invoker.Destroy()
-	}
-	return rpcService, destroy, nil
-}
-
 // buildMetadataServiceURL will use standard format to build the metadata service url.
 func buildMetadataServiceURL(serviceName string, host string, params map[string]string) *common.URL {
 	if params[constant.ProtocolKey] == "" {
@@ -108,7 +93,7 @@
 	if len(jsonStr) > 0 {
 		err := json.Unmarshal([]byte(jsonStr), &res)
 		if err != nil {
-			logger.Errorf("could not parse the metadata service url parameters to map", err)
+			logger.Errorf("could not parse the metadata service url parameters '%s' to map", jsonStr)
 		}
 	}
 	return res
diff --git a/metadata/client_test.go b/metadata/client_test.go
new file mode 100644
index 0000000..aa9fe76
--- /dev/null
+++ b/metadata/client_test.go
@@ -0,0 +1,280 @@
+/*
+ * 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 metadata
+
+import (
+	"context"
+	"testing"
+)
+
+import (
+	"github.com/pkg/errors"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+import (
+	"dubbo.apache.org/dubbo-go/v3/common"
+	"dubbo.apache.org/dubbo-go/v3/common/constant"
+	"dubbo.apache.org/dubbo-go/v3/common/extension"
+	"dubbo.apache.org/dubbo-go/v3/metadata/info"
+	"dubbo.apache.org/dubbo-go/v3/protocol"
+	_ "dubbo.apache.org/dubbo-go/v3/proxy/proxy_factory"
+	"dubbo.apache.org/dubbo-go/v3/registry"
+)
+
+var (
+	ins = &registry.DefaultServiceInstance{
+		ID: "1",
+		Metadata: map[string]string{
+			constant.MetadataServiceURLParamsPropertyName: `{
+				"application": "dubbo-go",
+				"group": "BDTService",
+				"port": "64658",
+				"protocol": "dubbo",
+				"version": "1.0.0"
+			}`,
+		},
+		Host:        "dubbo.io",
+		ServiceName: "dubbo-app",
+	}
+	metadataInfo = &info.MetadataInfo{
+		App: "dubbo-app",
+	}
+)
+
+func TestGetMetadataFromMetadataReport(t *testing.T) {
+	t.Run("no report instance", func(t *testing.T) {
+		_, err := GetMetadataFromMetadataReport("1", ins)
+		assert.NotNil(t, err)
+	})
+	mockReport := new(mockMetadataReport)
+	defer mockReport.AssertExpectations(t)
+	instances["default"] = mockReport
+	t.Run("normal", func(t *testing.T) {
+		mockReport.On("GetAppMetadata").Return(metadataInfo, nil).Once()
+		got, err := GetMetadataFromMetadataReport("1", ins)
+		assert.Nil(t, err)
+		assert.Equal(t, metadataInfo, got)
+	})
+	t.Run("error", func(t *testing.T) {
+		mockReport.On("GetAppMetadata").Return(metadataInfo, errors.New("mock error")).Once()
+		_, err := GetMetadataFromMetadataReport("1", ins)
+		assert.NotNil(t, err)
+	})
+}
+
+func TestGetMetadataFromRpc(t *testing.T) {
+	mockInvoker := new(mockInvoker)
+	defer mockInvoker.AssertExpectations(t)
+	mockProtocol := new(mockProtocol)
+	defer mockProtocol.AssertExpectations(t)
+	extension.SetProtocol("dubbo", func() protocol.Protocol {
+		return mockProtocol
+	})
+
+	result := &protocol.RPCResult{
+		Attrs: map[string]interface{}{},
+		Err:   nil,
+		Rest:  metadataInfo,
+	}
+	t.Run("normal", func(t *testing.T) {
+		mockProtocol.On("Refer").Return(mockInvoker).Once()
+		mockInvoker.On("Invoke").Return(result).Once()
+		mockInvoker.On("Destroy").Once()
+		metadata, err := GetMetadataFromRpc("111", ins)
+		assert.Nil(t, err)
+		assert.Equal(t, metadata, result.Rest)
+	})
+	t.Run("refer error", func(t *testing.T) {
+		mockProtocol.On("Refer").Return(nil).Once()
+		_, err := GetMetadataFromRpc("111", ins)
+		assert.NotNil(t, err)
+	})
+	t.Run("invoke timeout", func(t *testing.T) {
+		mockProtocol.On("Refer").Return(mockInvoker).Once()
+		mockInvoker.On("Invoke").Return(&protocol.RPCResult{
+			Attrs: map[string]interface{}{},
+			Err:   errors.New("timeout error"),
+			Rest:  metadataInfo,
+		}).Once()
+		mockInvoker.On("Destroy").Once()
+		_, err := GetMetadataFromRpc("111", ins)
+		assert.NotNil(t, err)
+	})
+}
+
+func Test_buildMetadataServiceURL(t *testing.T) {
+	type args struct {
+		serviceName string
+		host        string
+		params      map[string]string
+	}
+	tests := []struct {
+		name string
+		args args
+		want *common.URL
+	}{
+		{
+			name: "normal",
+			args: args{
+				serviceName: "dubbo-app",
+				host:        "dubbo.io",
+				params: map[string]string{
+					constant.ProtocolKey: "dubbo",
+					constant.PortKey:     "3000",
+				},
+			},
+			want: common.NewURLWithOptions(
+				common.WithIp("dubbo.io"),
+				common.WithProtocol("dubbo"),
+				common.WithPath(constant.MetadataServiceName),
+				common.WithProtocol("dubbo"),
+				common.WithPort("3000"),
+				common.WithParams(map[string][]string{
+					constant.ProtocolKey: {"dubbo"},
+					constant.PortKey:     {"3000"},
+				}),
+				common.WithParamsValue(constant.GroupKey, "dubbo-app"),
+				common.WithParamsValue(constant.InterfaceKey, constant.MetadataServiceName),
+			),
+		},
+		{
+			name: "no protocol",
+			args: args{
+				serviceName: "dubbo-app",
+				host:        "dubbo.io",
+				params:      map[string]string{},
+			},
+			want: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equalf(t, tt.want, buildMetadataServiceURL(tt.args.serviceName, tt.args.host, tt.args.params), "buildMetadataServiceURL(%v, %v, %v)", tt.args.serviceName, tt.args.host, tt.args.params)
+		})
+	}
+}
+
+func Test_getMetadataServiceUrlParams(t *testing.T) {
+	type args struct {
+		jsonStr string
+	}
+	tests := []struct {
+		name string
+		args args
+		want map[string]string
+	}{
+		{
+			name: "normal",
+			args: args{
+				jsonStr: `{
+					"application": "BDTService",
+					"group": "BDTService",
+					"port": "64658",
+					"protocol": "dubbo",
+					"release": "dubbo-golang-3.0.0",
+					"timestamp": "1713432877",
+					"version": "1.0.0"
+				}`,
+			},
+			want: map[string]string{
+				"application": "BDTService",
+				"group":       "BDTService",
+				"port":        "64658",
+				"protocol":    "dubbo",
+				"release":     "dubbo-golang-3.0.0",
+				"timestamp":   "1713432877",
+				"version":     "1.0.0",
+			},
+		},
+		{
+			name: "wrong format",
+			args: args{
+				jsonStr: "xxx",
+			},
+			want: map[string]string{},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equalf(t, tt.want, getMetadataServiceUrlParams(tt.args.jsonStr), "getMetadataServiceUrlParams(%v)", tt.args.jsonStr)
+		})
+	}
+}
+
+type mockProtocol struct {
+	mock.Mock
+}
+
+func (m *mockProtocol) Export(invoker protocol.Invoker) protocol.Exporter {
+	args := m.Called()
+	return args.Get(0).(protocol.Exporter)
+}
+
+func (m *mockProtocol) Refer(url *common.URL) protocol.Invoker {
+	args := m.Called()
+	if args.Get(0) == nil {
+		return nil
+	}
+	return args.Get(0).(protocol.Invoker)
+}
+
+func (m *mockProtocol) Destroy() {
+}
+
+type mockInvoker struct {
+	mock.Mock
+}
+
+func (m *mockInvoker) GetURL() *common.URL {
+	return nil
+}
+
+func (m *mockInvoker) IsAvailable() bool {
+	return true
+}
+
+func (m *mockInvoker) Destroy() {
+	m.Called()
+}
+
+func (m *mockInvoker) Invoke(ctx context.Context, inv protocol.Invocation) protocol.Result {
+	args := m.Mock.Called()
+	meta := args.Get(0).(protocol.Result).Result().(*info.MetadataInfo)
+	reply := inv.Reply().(*info.MetadataInfo)
+	reply.App = meta.App
+	reply.Tag = meta.Tag
+	reply.Revision = meta.Revision
+	reply.Services = meta.Services
+	return args.Get(0).(protocol.Result)
+}
+
+type mockExporter struct {
+	mock.Mock
+}
+
+func (m *mockExporter) GetInvoker() protocol.Invoker {
+	args := m.Called()
+	return args.Get(0).(protocol.Invoker)
+}
+
+func (m *mockExporter) UnExport() {
+	m.Called()
+}
diff --git a/metadata/info/metadata_info.go b/metadata/info/metadata_info.go
index 471a97d..126d27c 100644
--- a/metadata/info/metadata_info.go
+++ b/metadata/info/metadata_info.go
@@ -22,6 +22,7 @@
 	"hash/crc32"
 	"net/url"
 	"sort"
+	"strconv"
 	"strings"
 )
 
@@ -66,8 +67,18 @@
 	subscribedServiceURLs map[string][]*common.URL `hessian:"-"` // client subscribed service urls
 }
 
+func NewAppMetadataInfo(app string) *MetadataInfo {
+	return NewMetadataInfo(app, "")
+}
+
 func NewMetadataInfo(app, tag string) *MetadataInfo {
-	return NewMetadataInfoWithParams(app, "", make(map[string]*ServiceInfo))
+	return &MetadataInfo{
+		App:                   app,
+		Tag:                   tag,
+		Services:              make(map[string]*ServiceInfo),
+		exportedServiceURLs:   make(map[string][]*common.URL),
+		subscribedServiceURLs: make(map[string][]*common.URL),
+	}
 }
 
 func NewMetadataInfoWithParams(app string, revision string, services map[string]*ServiceInfo) *MetadataInfo {
@@ -207,9 +218,9 @@
 	URL        *common.URL `json:"-" hessian:"-"`
 }
 
-// nolint
 func NewServiceInfoWithURL(url *common.URL) *ServiceInfo {
 	service := NewServiceInfo(url.Service(), url.Group(), url.Version(), url.Protocol, url.Path, nil)
+	service.Port, _ = strconv.Atoi(url.Port)
 	service.URL = url
 	// TODO includeKeys load dynamic
 	p := make(map[string]string, 8)
@@ -231,7 +242,6 @@
 	return service
 }
 
-// nolint
 func NewServiceInfo(name, group, version, protocol, path string, params map[string]string) *ServiceInfo {
 	serviceKey := common.ServiceKey(name, group, version)
 	matchKey := common.MatchKey(serviceKey, protocol)
@@ -256,16 +266,13 @@
 	return strings.Split(s, ",")
 }
 
-// nolint
 func (si *ServiceInfo) GetParams() url.Values {
 	v := url.Values{}
-	methodNames := si.Params[constant.MethodsKey]
-	if len(methodNames) == 0 {
-		return v
-	}
 	methods := gxset.NewSet()
-	for _, method := range strings.Split(si.Params[constant.MethodsKey], ",") {
-		methods.Add(method)
+	if methodNames, ok := si.Params[constant.MethodsKey]; ok {
+		for _, method := range strings.Split(methodNames, ",") {
+			methods.Add(method)
+		}
 	}
 	for k, p := range si.Params {
 		ms := strings.Index(k, ".")
@@ -278,7 +285,6 @@
 	return v
 }
 
-// nolint
 func (si *ServiceInfo) GetMatchKey() string {
 	if si.MatchKey != "" {
 		return si.MatchKey
@@ -288,7 +294,6 @@
 	return si.MatchKey
 }
 
-// nolint
 func (si *ServiceInfo) GetServiceKey() string {
 	if si.ServiceKey != "" {
 		return si.ServiceKey
diff --git a/metadata/info/metadata_info_test.go b/metadata/info/metadata_info_test.go
index 41889bf..94cd6e0 100644
--- a/metadata/info/metadata_info_test.go
+++ b/metadata/info/metadata_info_test.go
@@ -19,6 +19,8 @@
 
 import (
 	"encoding/json"
+	"strconv"
+	"strings"
 	"testing"
 )
 
@@ -32,6 +34,19 @@
 	"dubbo.apache.org/dubbo-go/v3/common"
 )
 
+var (
+	serviceUrl = common.NewURLWithOptions(
+		common.WithProtocol("tri"),
+		common.WithIp("127.0.0.1"),
+		common.WithPort("20035"),
+		common.WithPath("/org.apache.dubbo.samples.proto.GreetService"),
+		common.WithInterface("org.apache.dubbo.samples.proto.GreetService"),
+		common.WithMethods([]string{"Greet", "SayHello"}),
+		common.WithParamsValue("loadbalance", "random"),
+		common.WithParamsValue("methods.Greet.timeout", "1000"),
+	)
+)
+
 func TestMetadataInfoAddService(t *testing.T) {
 	metadataInfo := &MetadataInfo{
 		Services:              make(map[string]*ServiceInfo),
@@ -42,11 +57,11 @@
 	url, _ := common.NewURL("dubbo://127.0.0.1:20000?application=foo&category=providers&check=false&dubbo=dubbo-go+v1.5.0&interface=com.foo.Bar&methods=GetPetByID%2CGetPetTypes&organization=Apache&owner=foo&revision=1.0.0&side=provider&version=1.0.0")
 	metadataInfo.AddService(url)
 	assert.True(t, len(metadataInfo.Services) > 0)
-	assert.True(t, len(metadataInfo.exportedServiceURLs) > 0)
+	assert.True(t, len(metadataInfo.GetExportedServiceURLs()) > 0)
 
 	metadataInfo.RemoveService(url)
 	assert.True(t, len(metadataInfo.Services) == 0)
-	assert.True(t, len(metadataInfo.exportedServiceURLs) == 0)
+	assert.True(t, len(metadataInfo.GetExportedServiceURLs()) == 0)
 }
 
 func TestHessian(t *testing.T) {
@@ -67,3 +82,78 @@
 	metaJson, _ := json.Marshal(metadataInfo)
 	assert.Equal(t, objJson, metaJson)
 }
+
+func TestMetadataInfoAddSubscribeURL(t *testing.T) {
+	info := NewMetadataInfo("dubbo", "tag")
+	info.AddSubscribeURL(serviceUrl)
+	assert.True(t, len(info.GetSubscribedURLs()) > 0)
+	info.RemoveSubscribeURL(serviceUrl)
+	assert.True(t, len(info.GetSubscribedURLs()) == 0)
+}
+
+func TestMetadataInfoCalAndGetRevision(t *testing.T) {
+	metadata := NewAppMetadataInfo("dubbo")
+	assert.Equalf(t, "0", metadata.CalAndGetRevision(), "CalAndGetRevision()")
+	metadata.AddService(serviceUrl)
+	assert.True(t, metadata.CalAndGetRevision() != "0")
+
+	v := metadata.Revision
+	assert.Equal(t, v, metadata.CalAndGetRevision(), "CalAndGetRevision() test cache")
+
+	metadata = NewAppMetadataInfo("dubbo")
+	url1 := serviceUrl.Clone()
+	url1.Methods = []string{}
+	metadata.AddService(url1)
+	assert.True(t, metadata.CalAndGetRevision() != "0", "CalAndGetRevision() test empty methods")
+}
+
+func TestNewMetadataInfo(t *testing.T) {
+	info := NewMetadataInfo("dubbo", "tag")
+	assert.Equal(t, info.App, "dubbo")
+	assert.Equal(t, info.Tag, "tag")
+}
+
+func TestNewMetadataInfoWithParams(t *testing.T) {
+	info := NewMetadataInfoWithParams("dubbo", "",
+		map[string]*ServiceInfo{"org.apache.dubbo.samples.proto.GreetService": NewServiceInfoWithURL(serviceUrl)})
+	assert.Equal(t, info.App, "dubbo")
+	assert.Equal(t, info.Revision, "")
+	assert.Equal(t, info.Services, map[string]*ServiceInfo{"org.apache.dubbo.samples.proto.GreetService": NewServiceInfoWithURL(serviceUrl)})
+}
+
+func TestNewServiceInfoWithURL(t *testing.T) {
+	info := NewServiceInfoWithURL(serviceUrl)
+	assert.True(t, info.URL == serviceUrl)
+	assert.Equal(t, info.Protocol, serviceUrl.Protocol)
+	assert.Equal(t, info.Name, serviceUrl.Interface())
+	assert.Equal(t, info.Group, serviceUrl.Group())
+	assert.Equal(t, info.Version, serviceUrl.Version())
+	assert.Equal(t, strconv.Itoa(info.Port), serviceUrl.Port)
+	assert.Equal(t, info.Path, strings.TrimPrefix(serviceUrl.Path, "/"))
+	assert.Equal(t, info.Params["Greet.timeout"], "1000")
+}
+
+func TestServiceInfoGetMethods(t *testing.T) {
+	service := NewServiceInfoWithURL(serviceUrl)
+	assert.Equal(t, service.GetMethods(), []string{"Greet", "SayHello"})
+}
+
+func TestServiceInfoGetParams(t *testing.T) {
+	service := NewServiceInfoWithURL(serviceUrl)
+	assert.Equal(t, service.GetParams()["loadbalance"], []string{"random"})
+}
+
+func TestServiceInfoGetMatchKey(t *testing.T) {
+	si := NewServiceInfoWithURL(serviceUrl)
+	matchKey := si.MatchKey
+	assert.Equal(t, si.GetMatchKey(), matchKey)
+	si.MatchKey = ""
+	assert.True(t, si.GetMatchKey() != "")
+	si.MatchKey = ""
+	si.ServiceKey = ""
+	assert.True(t, si.GetMatchKey() != "")
+}
+
+func TestServiceInfoJavaClassName(t *testing.T) {
+	assert.Equalf(t, "org.apache.dubbo.metadata.MetadataInfo", NewAppMetadataInfo("dubbo").JavaClassName(), "JavaClassName()")
+}
diff --git a/metadata/mapping/metadata/service_name_mapping.go b/metadata/mapping/metadata/service_name_mapping.go
index 95e588e..1346436 100644
--- a/metadata/mapping/metadata/service_name_mapping.go
+++ b/metadata/mapping/metadata/service_name_mapping.go
@@ -23,7 +23,6 @@
 
 import (
 	gxset "github.com/dubbogo/gost/container/set"
-	"github.com/dubbogo/gost/log/logger"
 
 	perrors "github.com/pkg/errors"
 )
@@ -81,7 +80,6 @@
 			}
 		}
 		if err != nil {
-			logger.Errorf("Failed registering mapping to remote, &v", err)
 			return err
 		}
 	}
diff --git a/metadata/mapping/metadata/service_name_mapping_test.go b/metadata/mapping/metadata/service_name_mapping_test.go
new file mode 100644
index 0000000..01b25f5
--- /dev/null
+++ b/metadata/mapping/metadata/service_name_mapping_test.go
@@ -0,0 +1,182 @@
+/*
+ * 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 metadata
+
+import (
+	"errors"
+	"testing"
+)
+
+import (
+	gxset "github.com/dubbogo/gost/container/set"
+	"github.com/dubbogo/gost/gof/observer"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+import (
+	"dubbo.apache.org/dubbo-go/v3/common"
+	"dubbo.apache.org/dubbo-go/v3/common/constant"
+	"dubbo.apache.org/dubbo-go/v3/common/extension"
+	"dubbo.apache.org/dubbo-go/v3/metadata"
+	"dubbo.apache.org/dubbo-go/v3/metadata/info"
+	"dubbo.apache.org/dubbo-go/v3/metadata/mapping"
+	"dubbo.apache.org/dubbo-go/v3/metadata/report"
+)
+
+func TestGetNameMappingInstance(t *testing.T) {
+	ins := GetNameMappingInstance()
+	assert.NotNil(t, ins)
+}
+
+func TestNoReportInstance(t *testing.T) {
+	ins := GetNameMappingInstance()
+	lis := &listener{}
+	serviceUrl := common.NewURLWithOptions(
+		common.WithInterface("org.apache.dubbo.samples.proto.GreetService"),
+		common.WithParamsValue(constant.ApplicationKey, "dubbo"),
+	)
+	_, err := ins.Get(serviceUrl, lis)
+	assert.NotNil(t, err, "test Get no report instance")
+	err = ins.Map(serviceUrl)
+	assert.NotNil(t, err, "test Map with no report instance")
+	err = ins.Remove(serviceUrl)
+	assert.NotNil(t, err, "test Remove with no report instance")
+}
+
+func TestServiceNameMappingGet(t *testing.T) {
+	ins := GetNameMappingInstance()
+	lis := &listener{}
+	serviceUrl := common.NewURLWithOptions(
+		common.WithInterface("org.apache.dubbo.samples.proto.GreetService"),
+		common.WithParamsValue(constant.ApplicationKey, "dubbo"),
+	)
+	mockReport, err := initMock()
+	assert.Nil(t, err)
+	t.Run("test normal", func(t *testing.T) {
+		mockReport.On("GetServiceAppMapping").Return(gxset.NewSet("dubbo"), nil).Once()
+		apps, er := ins.Get(serviceUrl, lis)
+		assert.Nil(t, er)
+		assert.True(t, !apps.Empty())
+	})
+	t.Run("test error", func(t *testing.T) {
+		mockReport.On("GetServiceAppMapping").Return(gxset.NewSet(), errors.New("mock error")).Once()
+		_, err = ins.Get(serviceUrl, lis)
+		assert.NotNil(t, err)
+	})
+	mockReport.AssertExpectations(t)
+}
+
+func TestServiceNameMappingMap(t *testing.T) {
+	ins := GetNameMappingInstance()
+	serviceUrl := common.NewURLWithOptions(
+		common.WithInterface("org.apache.dubbo.samples.proto.GreetService"),
+		common.WithParamsValue(constant.ApplicationKey, "dubbo"),
+	)
+	mockReport, err := initMock()
+	assert.Nil(t, err)
+	t.Run("test normal", func(t *testing.T) {
+		mockReport.On("RegisterServiceAppMapping").Return(nil).Once()
+		err = ins.Map(serviceUrl)
+		assert.Nil(t, err)
+	})
+	t.Run("test error", func(t *testing.T) {
+		mockReport.On("RegisterServiceAppMapping").Return(errors.New("mock error")).Times(retryTimes)
+		err = ins.Map(serviceUrl)
+		assert.NotNil(t, err, "test mapping error")
+	})
+	mockReport.AssertExpectations(t)
+}
+
+func TestServiceNameMappingRemove(t *testing.T) {
+	ins := GetNameMappingInstance()
+	serviceUrl := common.NewURLWithOptions(
+		common.WithInterface("org.apache.dubbo.samples.proto.GreetService"),
+		common.WithParamsValue(constant.ApplicationKey, "dubbo"),
+	)
+	mockReport, err := initMock()
+	assert.Nil(t, err)
+	t.Run("test normal", func(t *testing.T) {
+		mockReport.On("RemoveServiceAppMappingListener").Return(nil).Once()
+		err = ins.Remove(serviceUrl)
+		assert.Nil(t, err)
+	})
+	t.Run("test error", func(t *testing.T) {
+		mockReport.On("RemoveServiceAppMappingListener").Return(errors.New("mock error")).Once()
+		err = ins.Remove(serviceUrl)
+		assert.NotNil(t, err)
+	})
+	mockReport.AssertExpectations(t)
+}
+
+func initMock() (*mockMetadataReport, error) {
+	metadataReport := new(mockMetadataReport)
+	extension.SetMetadataReportFactory("mock", func() report.MetadataReportFactory {
+		return metadataReport
+	})
+	opts := metadata.NewReportOptions(
+		metadata.WithProtocol("mock"),
+		metadata.WithAddress("127.0.0.1"),
+	)
+	err := opts.Init()
+	return metadataReport, err
+}
+
+type listener struct {
+}
+
+func (l listener) OnEvent(e observer.Event) error {
+	return nil
+}
+
+func (l listener) Stop() {
+}
+
+type mockMetadataReport struct {
+	mock.Mock
+}
+
+func (m *mockMetadataReport) CreateMetadataReport(*common.URL) report.MetadataReport {
+	return m
+}
+
+func (m *mockMetadataReport) GetAppMetadata(string, string) (*info.MetadataInfo, error) {
+	args := m.Called()
+	return args.Get(0).(*info.MetadataInfo), args.Error(1)
+}
+
+func (m *mockMetadataReport) PublishAppMetadata(string, string, *info.MetadataInfo) error {
+	args := m.Called()
+	return args.Error(0)
+}
+
+func (m *mockMetadataReport) RegisterServiceAppMapping(string, string, string) error {
+	args := m.Called()
+	return args.Error(0)
+}
+
+func (m *mockMetadataReport) GetServiceAppMapping(string, string, mapping.MappingListener) (*gxset.HashSet, error) {
+	args := m.Called()
+	return args.Get(0).(*gxset.HashSet), args.Error(1)
+}
+
+func (m *mockMetadataReport) RemoveServiceAppMappingListener(string, string) error {
+	args := m.Called()
+	return args.Error(0)
+}
diff --git a/metadata/metadata.go b/metadata/metadata.go
index 6fbbfcd..03077bb 100644
--- a/metadata/metadata.go
+++ b/metadata/metadata.go
@@ -25,8 +25,8 @@
 )
 
 var (
-	metadataService    MetadataService = &DefaultMetadataService{}
-	appMetadataInfoMap                 = make(map[string]*info.MetadataInfo)
+	registryMetadataInfo                 = make(map[string]*info.MetadataInfo)
+	metadataService      MetadataService = &DefaultMetadataService{metadataMap: registryMetadataInfo}
 )
 
 func GetMetadataService() MetadataService {
@@ -34,25 +34,25 @@
 }
 
 func GetMetadataInfo(registryId string) *info.MetadataInfo {
-	return appMetadataInfoMap[registryId]
+	return registryMetadataInfo[registryId]
 }
 
 func AddService(registryId string, url *common.URL) {
-	if _, exist := appMetadataInfoMap[registryId]; !exist {
-		appMetadataInfoMap[registryId] = info.NewMetadataInfo(
+	if _, exist := registryMetadataInfo[registryId]; !exist {
+		registryMetadataInfo[registryId] = info.NewMetadataInfo(
 			url.GetParam(constant.ApplicationKey, ""),
 			url.GetParam(constant.ApplicationTagKey, ""),
 		)
 	}
-	appMetadataInfoMap[registryId].AddService(url)
+	registryMetadataInfo[registryId].AddService(url)
 }
 
 func AddSubscribeURL(registryId string, url *common.URL) {
-	if _, exist := appMetadataInfoMap[registryId]; !exist {
-		appMetadataInfoMap[registryId] = info.NewMetadataInfo(
+	if _, exist := registryMetadataInfo[registryId]; !exist {
+		registryMetadataInfo[registryId] = info.NewMetadataInfo(
 			url.GetParam(constant.ApplicationKey, ""),
 			url.GetParam(constant.ApplicationTagKey, ""),
 		)
 	}
-	appMetadataInfoMap[registryId].AddSubscribeURL(url)
+	registryMetadataInfo[registryId].AddSubscribeURL(url)
 }
diff --git a/metadata/metadata_service.go b/metadata/metadata_service.go
index 5a3591c..80eb159 100644
--- a/metadata/metadata_service.go
+++ b/metadata/metadata_service.go
@@ -40,8 +40,8 @@
 
 // version will be used by Version func
 const (
-	version              = "1.0.0"
-	allServiceInterfaces = "*"
+	version  = "1.0.0"
+	allMatch = "*"
 )
 
 // MetadataService is used to define meta data related behaviors
@@ -58,12 +58,11 @@
 	GetMetadataInfo(revision string) (*info.MetadataInfo, error)
 	// GetMetadataServiceURL will return the url of metadata service
 	GetMetadataServiceURL() (*common.URL, error)
-	// SetMetadataServiceURL exporter to set url of metadata service, will not be exported by exporter,cause no error return
-	SetMetadataServiceURL(*common.URL)
 }
 
 // DefaultMetadataService is store and query the metadata info in memory when each service registry
 type DefaultMetadataService struct {
+	metadataMap map[string]*info.MetadataInfo
 	metadataUrl *common.URL
 }
 
@@ -73,19 +72,16 @@
 
 // GetExportedURLs get all exported urls
 func (mts *DefaultMetadataService) GetExportedURLs(serviceInterface string, group string, version string, protocol string) ([]*common.URL, error) {
-	if allServiceInterfaces == serviceInterface {
-		return mts.GetExportedServiceURLs()
-	}
 	all, err := mts.GetExportedServiceURLs()
 	if err != nil {
 		return nil, err
 	}
 	urls := make([]*common.URL, 0)
 	for _, url := range all {
-		if url.GetParam(constant.InterfaceKey, "") == serviceInterface &&
-			url.GetParam(constant.GroupKey, "") == group &&
-			url.GetParam(constant.ProtocolKey, "") == protocol &&
-			url.GetParam(constant.VersionKey, "") == version {
+		if (url.Interface() == serviceInterface || serviceInterface == allMatch) &&
+			(url.Group() == group || group == allMatch) &&
+			(url.Protocol == protocol || protocol == allMatch) &&
+			(url.Version() == version || version == allMatch) {
 			urls = append(urls, url)
 		}
 	}
@@ -97,7 +93,7 @@
 	if revision == "" {
 		return nil, nil
 	}
-	for _, metadataInfo := range appMetadataInfoMap {
+	for _, metadataInfo := range mts.metadataMap {
 		if metadataInfo.Revision == revision {
 			return metadataInfo, nil
 		}
@@ -109,7 +105,7 @@
 // GetExportedServiceURLs get exported service urls
 func (mts *DefaultMetadataService) GetExportedServiceURLs() ([]*common.URL, error) {
 	urls := make([]*common.URL, 0)
-	for _, metadataInfo := range appMetadataInfoMap {
+	for _, metadataInfo := range mts.metadataMap {
 		urls = append(urls, metadataInfo.GetExportedServiceURLs()...)
 	}
 	return urls, nil
@@ -127,7 +123,7 @@
 
 func (mts *DefaultMetadataService) GetSubscribedURLs() ([]*common.URL, error) {
 	urls := make([]*common.URL, 0)
-	for _, metadataInfo := range appMetadataInfoMap {
+	for _, metadataInfo := range mts.metadataMap {
 		urls = append(urls, metadataInfo.GetSubscribedURLs()...)
 	}
 	return urls, nil
@@ -141,15 +137,15 @@
 	}
 }
 
-// ServiceExporter is the ConfigurableMetadataServiceExporter which implement MetadataServiceExporter interface
-type ServiceExporter struct {
+// serviceExporter export MetadataService with dubbo protocol
+type serviceExporter struct {
 	opts             *Options
 	service          MetadataService
 	protocolExporter protocol.Exporter
 }
 
 // Export will export the metadataService
-func (e *ServiceExporter) Export() error {
+func (e *serviceExporter) Export() error {
 	version, _ := e.service.Version()
 	var port string
 	if e.opts.port == 0 {
@@ -181,7 +177,7 @@
 	proxyFactory := extension.GetProxyFactory("")
 	invoker := proxyFactory.GetInvoker(ivkURL)
 	e.protocolExporter = extension.GetProtocol(ivkURL.Protocol).Export(invoker)
-	e.service.SetMetadataServiceURL(ivkURL)
+	e.service.(*DefaultMetadataService).SetMetadataServiceURL(ivkURL)
 	logger.Infof("[Metadata Service] The MetadataService exports urls : %v ", ivkURL)
 	return nil
 }
@@ -199,6 +195,6 @@
 }
 
 // UnExport will unExport the metadataService
-func (e *ServiceExporter) UnExport() {
+func (e *serviceExporter) UnExport() {
 	e.protocolExporter.UnExport()
 }
diff --git a/metadata/metadata_service_test.go b/metadata/metadata_service_test.go
new file mode 100644
index 0000000..0939955
--- /dev/null
+++ b/metadata/metadata_service_test.go
@@ -0,0 +1,377 @@
+/*
+ * 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 metadata
+
+import (
+	"strconv"
+	"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/common/extension"
+	"dubbo.apache.org/dubbo-go/v3/metadata/info"
+	"dubbo.apache.org/dubbo-go/v3/protocol"
+	_ "dubbo.apache.org/dubbo-go/v3/proxy/proxy_factory"
+)
+
+var (
+	url, _ = common.NewURL("dubbo://127.0.0.1:20000?application=foo&category=providers&check=false&dubbo=dubbo-go+v1.5.0&interface=com.foo.Bar&methods=GetPetByID%2CGetPetTypes&organization=Apache&owner=foo&revision=1.0.0&side=provider&version=1.0.0")
+)
+
+func newMetadataMap() map[string]*info.MetadataInfo {
+	metadataInfo := info.NewAppMetadataInfo("dubbo-app")
+	metadataInfo.Revision = "1"
+	metadataInfo.AddService(url)
+	metadataInfo.AddSubscribeURL(url)
+	registryMetadataInfo["default"] = metadataInfo
+	return map[string]*info.MetadataInfo{
+		"default": metadataInfo,
+	}
+}
+
+func TestDefaultMetadataServiceGetExportedServiceURLs(t *testing.T) {
+	mts := &DefaultMetadataService{
+		metadataMap: newMetadataMap(),
+	}
+	got, err := mts.GetExportedServiceURLs()
+	assert.Nil(t, err)
+	assert.True(t, len(got) == 1)
+	assert.Equal(t, url, got[0])
+}
+
+func TestDefaultMetadataServiceGetExportedURLs(t *testing.T) {
+	type args struct {
+		serviceInterface string
+		group            string
+		version          string
+		protocol         string
+	}
+	tests := []struct {
+		name string
+		args args
+		want []*common.URL
+	}{
+		{
+			name: "all exact",
+			args: args{
+				serviceInterface: url.Interface(),
+				group:            url.Group(),
+				version:          url.Version(),
+				protocol:         url.Protocol,
+			},
+			want: []*common.URL{url},
+		},
+		{
+			name: "interface *",
+			args: args{
+				serviceInterface: "*",
+				group:            url.Group(),
+				version:          url.Version(),
+				protocol:         url.Protocol,
+			},
+			want: []*common.URL{url},
+		},
+		{
+			name: "group *",
+			args: args{
+				serviceInterface: url.Interface(),
+				group:            "*",
+				version:          url.Version(),
+				protocol:         url.Protocol,
+			},
+			want: []*common.URL{url},
+		},
+		{
+			name: "version *",
+			args: args{
+				serviceInterface: url.Interface(),
+				group:            url.Group(),
+				version:          "*",
+				protocol:         url.Protocol,
+			},
+			want: []*common.URL{url},
+		},
+		{
+			name: "protocol *",
+			args: args{
+				serviceInterface: url.Interface(),
+				group:            url.Group(),
+				version:          url.Version(),
+				protocol:         "*",
+			},
+			want: []*common.URL{url},
+		},
+		{
+			name: "all *",
+			args: args{
+				serviceInterface: "*",
+				group:            "*",
+				version:          "*",
+				protocol:         "*",
+			},
+			want: []*common.URL{url},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mts := &DefaultMetadataService{
+				metadataMap: newMetadataMap(),
+			}
+			got, err := mts.GetExportedURLs(tt.args.serviceInterface, tt.args.group, tt.args.version, tt.args.protocol)
+			assert.Nil(t, err)
+			assert.Equalf(t, tt.want, got, "GetExportedURLs(%v, %v, %v, %v)", tt.args.serviceInterface, tt.args.group, tt.args.version, tt.args.protocol)
+		})
+	}
+}
+
+func TestDefaultMetadataServiceGetMetadataInfo(t *testing.T) {
+	type args struct {
+		revision string
+	}
+	tests := []struct {
+		name string
+		args args
+		want *info.MetadataInfo
+	}{
+		{
+			name: "normal",
+			args: args{
+				revision: "1",
+			},
+			want: newMetadataMap()["default"],
+		},
+		{
+			name: "empty revision",
+			args: args{
+				revision: "",
+			},
+			want: nil,
+		},
+		{
+			name: "revision not match",
+			args: args{
+				revision: "2",
+			},
+			want: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mts := &DefaultMetadataService{
+				metadataMap: newMetadataMap(),
+			}
+			got, err := mts.GetMetadataInfo(tt.args.revision)
+			assert.Nil(t, err)
+			assert.Equalf(t, tt.want, got, "GetMetadataInfo(%v)", tt.args.revision)
+		})
+	}
+}
+
+func TestDefaultMetadataServiceGetMetadataServiceURL(t *testing.T) {
+	type fields struct {
+		metadataUrl *common.URL
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		want   *common.URL
+	}{
+		{
+			name: "normal",
+			fields: fields{
+				metadataUrl: url,
+			},
+			want: url,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mts := &DefaultMetadataService{
+				metadataUrl: tt.fields.metadataUrl,
+			}
+			got, err := mts.GetMetadataServiceURL()
+			assert.Nil(t, err)
+			assert.Equalf(t, tt.want, got, "GetMetadataServiceURL()")
+		})
+	}
+}
+
+func TestDefaultMetadataServiceGetSubscribedURLs(t *testing.T) {
+	tests := []struct {
+		name string
+		want []*common.URL
+	}{
+		{
+			name: "normal",
+			want: []*common.URL{url},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mts := &DefaultMetadataService{
+				metadataMap: newMetadataMap(),
+			}
+			got, err := mts.GetSubscribedURLs()
+			assert.Nil(t, err)
+			assert.Equalf(t, tt.want, got, "GetSubscribedURLs()")
+		})
+	}
+}
+
+func TestDefaultMetadataServiceMethodMapper(t *testing.T) {
+	tests := []struct {
+		name string
+		want map[string]string
+	}{
+		{
+			name: "normal",
+			want: map[string]string{
+				"GetExportedURLs": "getExportedURLs",
+				"GetMetadataInfo": "getMetadataInfo",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mts := &DefaultMetadataService{
+				metadataMap: newMetadataMap(),
+			}
+			assert.Equalf(t, tt.want, mts.MethodMapper(), "MethodMapper()")
+		})
+	}
+}
+
+func TestDefaultMetadataServiceSetMetadataServiceURL(t *testing.T) {
+	type args struct {
+		url *common.URL
+	}
+	tests := []struct {
+		name string
+		args args
+		want *common.URL
+	}{
+		{
+			name: "normal",
+			args: args{
+				url: url,
+			},
+			want: url,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mts := &DefaultMetadataService{
+				metadataMap: map[string]*info.MetadataInfo{},
+			}
+			mts.SetMetadataServiceURL(tt.args.url)
+			assert.Equal(t, tt.want, mts.metadataUrl)
+		})
+	}
+}
+
+func TestDefaultMetadataServiceVersion(t *testing.T) {
+	mts := &DefaultMetadataService{}
+	got, err := mts.Version()
+	assert.Nil(t, err)
+	assert.Equal(t, version, got)
+}
+
+func Test_randomPort(t *testing.T) {
+	port := randomPort()
+	assert.True(t, port != "")
+}
+
+func Test_serviceExporterExport(t *testing.T) {
+	mockExporter := new(mockExporter)
+	defer mockExporter.AssertExpectations(t)
+	mockProtocol := new(mockProtocol)
+	defer mockProtocol.AssertExpectations(t)
+	extension.SetProtocol("dubbo", func() protocol.Protocol {
+		return mockProtocol
+	})
+	t.Run("normal", func(t *testing.T) {
+		port := randomPort()
+		p, err := strconv.Atoi(port)
+		assert.Nil(t, err)
+		opts := &Options{
+			appName:      "dubbo-app",
+			metadataType: constant.RemoteMetadataStorageType,
+			port:         p,
+		}
+		mockProtocol.On("Export").Return(mockExporter).Once()
+		mockExporter.On("UnExport").Once()
+		e := &serviceExporter{
+			opts:    opts,
+			service: &DefaultMetadataService{},
+		}
+		err = e.Export()
+		assert.Nil(t, err)
+		e.UnExport()
+	})
+	// first t.Run has called commom.ServiceMap.Register ,second will fail
+	t.Run("get methods error", func(t *testing.T) {
+		port := randomPort()
+		p, err := strconv.Atoi(port)
+		assert.Nil(t, err)
+		opts := &Options{
+			appName:      "dubbo-app",
+			metadataType: constant.RemoteMetadataStorageType,
+			port:         p,
+		}
+		e := &serviceExporter{
+			opts:    opts,
+			service: &DefaultMetadataService{},
+		}
+		err = e.Export()
+		assert.NotNil(t, err)
+	})
+	t.Run("port == 0", func(t *testing.T) {
+		opts := &Options{
+			appName:      "dubbo-app",
+			metadataType: constant.RemoteMetadataStorageType,
+			port:         0,
+		}
+		// UnRegister first otherwise will fail
+		err := common.ServiceMap.UnRegister(constant.MetadataServiceName, constant.DefaultProtocol,
+			common.ServiceKey(constant.MetadataServiceName, opts.appName, version))
+		assert.Nil(t, err)
+		mockProtocol.On("Export").Return(mockExporter).Once()
+		mockExporter.On("UnExport").Once()
+		e := &serviceExporter{
+			opts:    opts,
+			service: &DefaultMetadataService{},
+		}
+		err = e.Export()
+		assert.Nil(t, err)
+		e.UnExport()
+	})
+}
+
+func Test_serviceExporterUnExport(t *testing.T) {
+	mockExporter := new(mockExporter)
+	defer mockExporter.AssertExpectations(t)
+	serviceExporter := &serviceExporter{protocolExporter: mockExporter}
+	mockExporter.On("UnExport").Once()
+	serviceExporter.UnExport()
+}
diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go
new file mode 100644
index 0000000..ae89077
--- /dev/null
+++ b/metadata/metadata_test.go
@@ -0,0 +1,141 @@
+/*
+ * 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 metadata
+
+import (
+	"reflect"
+	"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 TestAddService(t *testing.T) {
+	type args struct {
+		registryId string
+		url        *common.URL
+	}
+	tests := []struct {
+		name string
+		args args
+	}{
+		{
+			name: "add",
+			args: args{
+				registryId: "reg1",
+				url: common.NewURLWithOptions(
+					common.WithProtocol("dubbo"),
+					common.WithParamsValue(constant.ApplicationKey, "dubbo"),
+					common.WithParamsValue(constant.ApplicationTagKey, "v1"),
+				),
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			AddService(tt.args.registryId, tt.args.url)
+			assert.True(t, registryMetadataInfo[tt.args.registryId] != nil)
+			meta := registryMetadataInfo[tt.args.registryId]
+			meta.App = tt.args.url.GetParam(constant.ApplicationKey, "")
+			meta.Tag = tt.args.url.GetParam(constant.ApplicationTagKey, "")
+			assert.True(t, reflect.DeepEqual(meta.GetExportedServiceURLs()[0], tt.args.url))
+		})
+	}
+}
+
+func TestAddSubscribeURL(t *testing.T) {
+	type args struct {
+		registryId string
+		url        *common.URL
+	}
+	tests := []struct {
+		name string
+		args args
+	}{
+		{
+			name: "addSub",
+			args: args{
+				registryId: "reg2",
+				url: common.NewURLWithOptions(
+					common.WithProtocol("dubbo"),
+					common.WithParamsValue(constant.ApplicationKey, "dubbo"),
+					common.WithParamsValue(constant.ApplicationTagKey, "v1"),
+				),
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			AddSubscribeURL(tt.args.registryId, tt.args.url)
+			assert.True(t, registryMetadataInfo[tt.args.registryId] != nil)
+			meta := registryMetadataInfo[tt.args.registryId]
+			meta.App = tt.args.url.GetParam(constant.ApplicationKey, "")
+			meta.Tag = tt.args.url.GetParam(constant.ApplicationTagKey, "")
+			assert.True(t, reflect.DeepEqual(meta.GetSubscribedURLs()[0], tt.args.url))
+		})
+	}
+}
+
+func TestGetMetadataInfo(t *testing.T) {
+	type args struct {
+		registryId string
+	}
+	tests := []struct {
+		name string
+		args args
+		want *info.MetadataInfo
+	}{
+		{
+			name: "get",
+			args: args{
+				registryId: "reg3",
+			},
+			want: info.NewMetadataInfo("dubbo", "v2"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			registryMetadataInfo[tt.args.registryId] = tt.want
+			assert.Equalf(t, tt.want, GetMetadataInfo(tt.args.registryId), "GetMetadataInfo(%v)", tt.args.registryId)
+		})
+	}
+}
+
+func TestGetMetadataService(t *testing.T) {
+	tests := []struct {
+		name string
+		want MetadataService
+	}{
+		{
+			name: "getMetadataService",
+			want: metadataService,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equalf(t, tt.want, GetMetadataService(), "GetMetadataService()")
+		})
+	}
+}
diff --git a/metadata/options.go b/metadata/options.go
index 30e866a..5a5eb5b 100644
--- a/metadata/options.go
+++ b/metadata/options.go
@@ -26,11 +26,13 @@
 
 import (
 	"github.com/dubbogo/gost/log/logger"
+
+	perrors "github.com/pkg/errors"
 )
 
 import (
+	"dubbo.apache.org/dubbo-go/v3/common"
 	"dubbo.apache.org/dubbo-go/v3/common/constant"
-	"dubbo.apache.org/dubbo-go/v3/common/extension"
 	"dubbo.apache.org/dubbo-go/v3/global"
 )
 
@@ -62,7 +64,7 @@
 	var err error
 	exportOnce.Do(func() {
 		if opts.metadataType != constant.RemoteMetadataStorageType {
-			exporter := &ServiceExporter{service: metadataService, opts: opts}
+			exporter := &serviceExporter{service: metadataService, opts: opts}
 			defer func() {
 				// TODO remove this recover func,this just to avoid some unit test failed,this will not happen in user side mostly
 				// config test -> metadata exporter -> dubbo protocol/remoting -> config,cycle import will occur
@@ -103,18 +105,33 @@
 }
 
 func (opts *ReportOptions) Init() error {
-	fac := extension.GetMetadataReportFactory(opts.Protocol)
-	if fac == nil {
-		logger.Errorf("no metadata report factory of protocol %s found!", opts.Protocol)
-		return nil
-	}
-	url, err := toUrl(opts)
+	url, err := opts.toUrl()
 	if err != nil {
 		logger.Errorf("metadata report create error %v", err)
 		return err
 	}
-	instances[opts.registryId] = &DelegateMetadataReport{instance: fac.CreateMetadataReport(url)}
-	return nil
+	return addMetadataReport(opts.registryId, url)
+}
+
+func (opts *ReportOptions) toUrl() (*common.URL, error) {
+	res, err := common.NewURL(opts.Address,
+		common.WithUsername(opts.Username),
+		common.WithPassword(opts.Password),
+		common.WithLocation(opts.Address),
+		common.WithProtocol(opts.Protocol),
+		common.WithParamsValue(constant.TimeoutKey, opts.Timeout),
+		common.WithParamsValue(constant.MetadataReportGroupKey, opts.Group),
+		common.WithParamsValue(constant.MetadataReportNamespaceKey, opts.Namespace),
+		common.WithParamsValue(constant.ClientNameKey, strings.Join([]string{constant.MetadataReportPrefix, opts.Protocol, opts.Address}, "-")),
+	)
+	if err != nil || len(res.Protocol) == 0 {
+		return nil, perrors.New("Invalid MetadataReport Config.")
+	}
+	res.SetParam("metadata", res.Protocol)
+	for key, val := range opts.Params {
+		res.SetParam(key, val)
+	}
+	return res, nil
 }
 
 func defaultReportOptions() *ReportOptions {
diff --git a/metadata/report/report_factory.go b/metadata/report/report_factory.go
index 2c2af73..cd2cf7c 100644
--- a/metadata/report/report_factory.go
+++ b/metadata/report/report_factory.go
@@ -25,5 +25,3 @@
 type MetadataReportFactory interface {
 	CreateMetadataReport(*common.URL) MetadataReport
 }
-
-type BaseMetadataReportFactory struct{}
diff --git a/metadata/report_instance.go b/metadata/report_instance.go
index 2238144..f7bceb4 100644
--- a/metadata/report_instance.go
+++ b/metadata/report_instance.go
@@ -18,19 +18,18 @@
 package metadata
 
 import (
-	"strings"
 	"time"
 )
 
 import (
 	"github.com/dubbogo/gost/container/set"
-
-	perrors "github.com/pkg/errors"
+	"github.com/dubbogo/gost/log/logger"
 )
 
 import (
 	"dubbo.apache.org/dubbo-go/v3/common"
 	"dubbo.apache.org/dubbo-go/v3/common/constant"
+	"dubbo.apache.org/dubbo-go/v3/common/extension"
 	"dubbo.apache.org/dubbo-go/v3/metadata/info"
 	"dubbo.apache.org/dubbo-go/v3/metadata/mapping"
 	"dubbo.apache.org/dubbo-go/v3/metadata/report"
@@ -42,25 +41,14 @@
 	instances = make(map[string]report.MetadataReport)
 )
 
-func toUrl(opts *ReportOptions) (*common.URL, error) {
-	res, err := common.NewURL(opts.Address,
-		common.WithUsername(opts.Username),
-		common.WithPassword(opts.Password),
-		common.WithLocation(opts.Address),
-		common.WithProtocol(opts.Protocol),
-		common.WithParamsValue(constant.TimeoutKey, opts.Timeout),
-		common.WithParamsValue(constant.MetadataReportGroupKey, opts.Group),
-		common.WithParamsValue(constant.MetadataReportNamespaceKey, opts.Namespace),
-		common.WithParamsValue(constant.ClientNameKey, strings.Join([]string{constant.MetadataReportPrefix, opts.Protocol, opts.Address}, "-")),
-	)
-	if err != nil || len(res.Protocol) == 0 {
-		return nil, perrors.New("Invalid MetadataReport Config.")
+func addMetadataReport(registryId string, url *common.URL) error {
+	fac := extension.GetMetadataReportFactory(url.Protocol)
+	if fac == nil {
+		logger.Warnf("no metadata report factory of protocol %s found, please check if the metadata report factory is imported", url.Protocol)
+		return nil
 	}
-	res.SetParam("metadata", res.Protocol)
-	for key, val := range opts.Params {
-		res.SetParam(key, val)
-	}
-	return res, nil
+	instances[registryId] = &DelegateMetadataReport{instance: fac.CreateMetadataReport(url)}
+	return nil
 }
 
 func GetMetadataReport() report.MetadataReport {
@@ -91,6 +79,9 @@
 }
 
 func GetMetadataType() string {
+	if metadataOptions == nil || metadataOptions.metadataType == "" {
+		return constant.DefaultMetadataStorageType
+	}
 	return metadataOptions.metadataType
 }
 
diff --git a/metadata/report_instance_test.go b/metadata/report_instance_test.go
new file mode 100644
index 0000000..4b89cde
--- /dev/null
+++ b/metadata/report_instance_test.go
@@ -0,0 +1,264 @@
+/*
+ * 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 metadata
+
+import (
+	"reflect"
+	"testing"
+)
+
+import (
+	gxset "github.com/dubbogo/gost/container/set"
+	"github.com/dubbogo/gost/gof/observer"
+
+	"github.com/pkg/errors"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+import (
+	"dubbo.apache.org/dubbo-go/v3/common"
+	"dubbo.apache.org/dubbo-go/v3/common/constant"
+	"dubbo.apache.org/dubbo-go/v3/common/extension"
+	"dubbo.apache.org/dubbo-go/v3/metadata/info"
+	"dubbo.apache.org/dubbo-go/v3/metadata/mapping"
+	"dubbo.apache.org/dubbo-go/v3/metadata/report"
+	"dubbo.apache.org/dubbo-go/v3/metrics"
+	metricsMetadata "dubbo.apache.org/dubbo-go/v3/metrics/metadata"
+)
+
+func TestDelegateMetadataReportGetAppMetadata(t *testing.T) {
+	mockReport := new(mockMetadataReport)
+	defer mockReport.AssertExpectations(t)
+	delegate := &DelegateMetadataReport{instance: mockReport}
+	metadataInfo := info.NewAppMetadataInfo("dubbo")
+	var ch = make(chan metrics.MetricsEvent, 10)
+	metrics.Subscribe(constant.MetricsMetadata, ch)
+	defer close(ch)
+	t.Run("normal", func(t *testing.T) {
+		mockReport.On("GetAppMetadata").Return(metadataInfo, nil).Once()
+		got, err := delegate.GetAppMetadata("dubbo", "1")
+		assert.Nil(t, err)
+		if !reflect.DeepEqual(got, metadataInfo) {
+			t.Errorf("GetAppMetadata() got = %v, want %v", got, metadataInfo)
+		}
+		assert.True(t, len(ch) == 1)
+		metricEvent := <-ch
+		assert.Equal(t, metricEvent.Type(), constant.MetricsMetadata)
+		event, ok := metricEvent.(*metricsMetadata.MetadataMetricEvent)
+		assert.True(t, ok)
+		assert.NotNil(t, event.Name, metricsMetadata.MetadataSub)
+		assert.NotNil(t, event.Start)
+		assert.NotNil(t, event.End)
+		assert.True(t, event.Succ)
+	})
+	t.Run("error", func(t *testing.T) {
+		mockReport.On("GetAppMetadata").Return(info.NewAppMetadataInfo("dubbo"), errors.New("mock error")).Once()
+		_, err := delegate.GetAppMetadata("dubbo", "1111")
+		assert.NotNil(t, err)
+		assert.True(t, len(ch) == 1)
+		metricEvent := <-ch
+		assert.Equal(t, metricEvent.Type(), constant.MetricsMetadata)
+		event, ok := metricEvent.(*metricsMetadata.MetadataMetricEvent)
+		assert.True(t, ok)
+		assert.NotNil(t, event.Name, metricsMetadata.MetadataSub)
+		assert.NotNil(t, event.Start)
+		assert.NotNil(t, event.End)
+		assert.True(t, !event.Succ)
+	})
+}
+
+func TestDelegateMetadataReportPublishAppMetadata(t *testing.T) {
+	mockReport := new(mockMetadataReport)
+	defer mockReport.AssertExpectations(t)
+	delegate := &DelegateMetadataReport{instance: mockReport}
+	metadataInfo := info.NewAppMetadataInfo("dubbo")
+	var ch = make(chan metrics.MetricsEvent, 10)
+	metrics.Subscribe(constant.MetricsMetadata, ch)
+	defer close(ch)
+	t.Run("normal", func(t *testing.T) {
+		mockReport.On("PublishAppMetadata").Return(nil).Once()
+		err := delegate.PublishAppMetadata("application", "revision", metadataInfo)
+		assert.Nil(t, err)
+		assert.True(t, len(ch) == 1)
+		metricEvent := <-ch
+		assert.Equal(t, metricEvent.Type(), constant.MetricsMetadata)
+		event, ok := metricEvent.(*metricsMetadata.MetadataMetricEvent)
+		assert.True(t, ok)
+		assert.NotNil(t, event.Name, metricsMetadata.MetadataPush)
+		assert.NotNil(t, event.Start)
+		assert.NotNil(t, event.End)
+		assert.True(t, event.Succ)
+	})
+	t.Run("error", func(t *testing.T) {
+		mockReport.On("PublishAppMetadata").Return(errors.New("mock error")).Once()
+		err := delegate.PublishAppMetadata("application", "revision", metadataInfo)
+		assert.NotNil(t, err)
+		assert.True(t, len(ch) == 1)
+		metricEvent := <-ch
+		assert.Equal(t, metricEvent.Type(), constant.MetricsMetadata)
+		event, ok := metricEvent.(*metricsMetadata.MetadataMetricEvent)
+		assert.True(t, ok)
+		assert.NotNil(t, event.Name, metricsMetadata.MetadataPush)
+		assert.NotNil(t, event.Start)
+		assert.NotNil(t, event.End)
+		assert.True(t, !event.Succ)
+	})
+}
+
+func TestDelegateMetadataReportGetServiceAppMapping(t *testing.T) {
+	mockReport := new(mockMetadataReport)
+	defer mockReport.AssertExpectations(t)
+	delegate := &DelegateMetadataReport{instance: mockReport}
+	t.Run("normal", func(t *testing.T) {
+		mockReport.On("GetServiceAppMapping").Return(gxset.NewSet(), nil).Once()
+		got, err := delegate.GetServiceAppMapping("dubbo", "dev", &listener{})
+		assert.Nil(t, err)
+		assert.True(t, got.Empty())
+	})
+	t.Run("error", func(t *testing.T) {
+		mockReport.On("GetServiceAppMapping").Return(gxset.NewSet(), errors.New("mock error")).Once()
+		_, err := delegate.GetServiceAppMapping("dubbo", "dev", &listener{})
+		assert.NotNil(t, err)
+	})
+}
+
+func TestDelegateMetadataReportRegisterServiceAppMapping(t *testing.T) {
+	mockReport := new(mockMetadataReport)
+	defer mockReport.AssertExpectations(t)
+	delegate := &DelegateMetadataReport{instance: mockReport}
+	t.Run("normal", func(t *testing.T) {
+		mockReport.On("RegisterServiceAppMapping").Return(nil).Once()
+		err := delegate.RegisterServiceAppMapping("interfaceName", "group", "application")
+		assert.Nil(t, err)
+	})
+	t.Run("error", func(t *testing.T) {
+		mockReport.On("RegisterServiceAppMapping").Return(errors.New("mock error")).Once()
+		err := delegate.RegisterServiceAppMapping("interfaceName", "group", "application")
+		assert.NotNil(t, err)
+	})
+}
+
+func TestDelegateMetadataReportRemoveServiceAppMappingListener(t *testing.T) {
+	mockReport := new(mockMetadataReport)
+	defer mockReport.AssertExpectations(t)
+	delegate := &DelegateMetadataReport{instance: mockReport}
+	t.Run("normal", func(t *testing.T) {
+		mockReport.On("RemoveServiceAppMappingListener").Return(nil).Once()
+		err := delegate.RemoveServiceAppMappingListener("interfaceName", "group")
+		assert.Nil(t, err)
+	})
+	t.Run("error", func(t *testing.T) {
+		mockReport.On("RemoveServiceAppMappingListener").Return(errors.New("mock error")).Once()
+		err := delegate.RemoveServiceAppMappingListener("interfaceName", "group")
+		assert.NotNil(t, err)
+	})
+}
+
+func TestGetMetadataReport(t *testing.T) {
+	instances = make(map[string]report.MetadataReport)
+	assert.Nil(t, GetMetadataReport())
+	instances["default"] = new(mockMetadataReport)
+	assert.NotNil(t, GetMetadataReport())
+}
+
+func TestGetMetadataReportByRegistry(t *testing.T) {
+	instances = make(map[string]report.MetadataReport)
+	assert.Nil(t, GetMetadataReportByRegistry("reg"))
+	instances["default"] = new(mockMetadataReport)
+	assert.NotNil(t, GetMetadataReportByRegistry("default"))
+	assert.NotNil(t, GetMetadataReportByRegistry("reg"))
+	assert.NotNil(t, GetMetadataReportByRegistry(""))
+}
+
+func TestGetMetadataReports(t *testing.T) {
+	instances = make(map[string]report.MetadataReport)
+	assert.True(t, len(GetMetadataReports()) == 0)
+	instances["default"] = new(mockMetadataReport)
+	assert.True(t, len(GetMetadataReports()) == 1)
+}
+
+func TestGetMetadataType(t *testing.T) {
+	assert.Equal(t, GetMetadataType(), constant.DefaultMetadataStorageType)
+	metadataOptions = &Options{}
+	assert.Equal(t, GetMetadataType(), constant.DefaultMetadataStorageType)
+	metadataOptions = &Options{
+		metadataType: constant.RemoteMetadataStorageType,
+	}
+	assert.Equal(t, GetMetadataType(), constant.RemoteMetadataStorageType)
+}
+
+func TestAddMetadataReport(t *testing.T) {
+	url := common.NewURLWithOptions(
+		common.WithProtocol("registryId"),
+	)
+	err := addMetadataReport("registryId", url)
+	assert.Nil(t, err)
+	assert.True(t, instances["registryId"] == nil)
+	mockReport := new(mockMetadataReport)
+	extension.SetMetadataReportFactory("registryId", func() report.MetadataReportFactory {
+		return mockReport
+	})
+	err = addMetadataReport("registryId", url)
+	assert.Nil(t, err)
+	assert.True(t, instances["registryId"] != nil)
+}
+
+type mockMetadataReport struct {
+	mock.Mock
+}
+
+func (m *mockMetadataReport) CreateMetadataReport(*common.URL) report.MetadataReport {
+	return m
+}
+
+func (m *mockMetadataReport) GetAppMetadata(string, string) (*info.MetadataInfo, error) {
+	args := m.Called()
+	return args.Get(0).(*info.MetadataInfo), args.Error(1)
+}
+
+func (m *mockMetadataReport) PublishAppMetadata(string, string, *info.MetadataInfo) error {
+	args := m.Called()
+	return args.Error(0)
+}
+
+func (m *mockMetadataReport) RegisterServiceAppMapping(string, string, string) error {
+	args := m.Called()
+	return args.Error(0)
+}
+
+func (m *mockMetadataReport) GetServiceAppMapping(string, string, mapping.MappingListener) (*gxset.HashSet, error) {
+	args := m.Called()
+	return args.Get(0).(*gxset.HashSet), args.Error(1)
+}
+
+func (m *mockMetadataReport) RemoveServiceAppMappingListener(string, string) error {
+	args := m.Called()
+	return args.Error(0)
+}
+
+type listener struct {
+}
+
+func (l *listener) OnEvent(e observer.Event) error {
+	return nil
+}
+
+func (l *listener) Stop() {
+}
diff --git a/options.go b/options.go
index b4bff1d..5fd162d 100644
--- a/options.go
+++ b/options.go
@@ -94,7 +94,6 @@
 		log.Infof("[Config Center] Config center doesn't start")
 		log.Debugf("config center doesn't start because %s", err)
 	} else {
-		compatInstanceOptions(rcCompat, rc)
 		if err = rcCompat.Logger.Init(); err != nil { // init logger using config from config center again
 			return err
 		}
@@ -110,15 +109,8 @@
 	}
 
 	// init protocol
-	protocols := rcCompat.Protocols
-	if len(protocols) <= 0 {
-		protocol := &config.ProtocolConfig{}
-		protocols = make(map[string]*config.ProtocolConfig, 1)
-		protocols[constant.Dubbo] = protocol
-		rcCompat.Protocols = protocols
-	}
-	for _, protocol := range protocols {
-		if err := protocol.Init(); err != nil {
+	for _, protocolConfig := range rcCompat.Protocols {
+		if err := protocolConfig.Init(); err != nil {
 			return err
 		}
 	}
@@ -169,6 +161,7 @@
 		return err
 	}
 
+	compatInstanceOptions(rcCompat, rc) // overrider options config because some config are changed after init
 	return nil
 }
 
diff --git a/registry/servicediscovery/customizer/service_instance_host_port_customizer.go b/registry/servicediscovery/customizer/service_instance_host_port_customizer.go
index f33023f..bd918d6 100644
--- a/registry/servicediscovery/customizer/service_instance_host_port_customizer.go
+++ b/registry/servicediscovery/customizer/service_instance_host_port_customizer.go
@@ -39,7 +39,7 @@
 
 // Customize calculate the revision for exported urls and then put it into instance metadata
 func (e *hostPortCustomizer) Customize(instance registry.ServiceInstance) {
-	if instance.GetPort() > 0 {
+	if instance.GetPort() > 0 { // has set, avoid reset
 		return
 	}
 	if instance.GetServiceMetadata() == nil || len(instance.GetServiceMetadata().GetExportedServiceURLs()) == 0 {