You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by al...@apache.org on 2025/12/21 23:48:04 UTC

(dubbo-go) branch develop updated: test(triple): enhance unit tests for protocol/triple package (#3133)

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 96053821e test(triple): enhance unit tests for protocol/triple package (#3133)
96053821e is described below

commit 96053821e888aaaeda7b4f6875b099d00c06f9c8
Author: CAICAII <33...@qq.com>
AuthorDate: Mon Dec 22 07:43:36 2025 +0800

    test(triple): enhance unit tests for protocol/triple package (#3133)
---
 protocol/triple/client_test.go         | 560 +++++++++++++++++++++++++++++++++
 protocol/triple/server_test.go         | 343 +++++++++++++++++++-
 protocol/triple/triple_invoker_test.go | 456 +++++++++++++++++++++++++++
 protocol/triple/triple_test.go         |  69 ++++
 4 files changed, 1427 insertions(+), 1 deletion(-)

diff --git a/protocol/triple/client_test.go b/protocol/triple/client_test.go
index 8f39c4458..4f40e6f74 100644
--- a/protocol/triple/client_test.go
+++ b/protocol/triple/client_test.go
@@ -18,6 +18,7 @@
 package triple
 
 import (
+	"context"
 	"net/http"
 	"testing"
 	"time"
@@ -31,6 +32,7 @@ import (
 	"dubbo.apache.org/dubbo-go/v3/common"
 	"dubbo.apache.org/dubbo-go/v3/common/constant"
 	"dubbo.apache.org/dubbo-go/v3/global"
+	tri "dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
 )
 
 func TestClientManager_HTTP2AndHTTP3(t *testing.T) {
@@ -94,3 +96,561 @@ func TestDualTransport(t *testing.T) {
 	})
 	assert.True(t, ok, "transport should implement http.RoundTripper")
 }
+
+func TestClientManager_GetClient(t *testing.T) {
+	tests := []struct {
+		desc      string
+		cm        *clientManager
+		method    string
+		expectErr bool
+	}{
+		{
+			desc: "method exists",
+			cm: &clientManager{
+				triClients: map[string]*tri.Client{
+					"TestMethod": tri.NewClient(&http.Client{}, "http://localhost:8080/test"),
+				},
+			},
+			method:    "TestMethod",
+			expectErr: false,
+		},
+		{
+			desc: "method not exists",
+			cm: &clientManager{
+				triClients: map[string]*tri.Client{
+					"TestMethod": tri.NewClient(&http.Client{}, "http://localhost:8080/test"),
+				},
+			},
+			method:    "NonExistMethod",
+			expectErr: true,
+		},
+		{
+			desc: "empty triClients",
+			cm: &clientManager{
+				triClients: map[string]*tri.Client{},
+			},
+			method:    "AnyMethod",
+			expectErr: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			client, err := test.cm.getClient(test.method)
+			if test.expectErr {
+				assert.NotNil(t, err)
+				assert.Nil(t, client)
+				assert.Contains(t, err.Error(), "missing triple client")
+			} else {
+				assert.Nil(t, err)
+				assert.NotNil(t, client)
+			}
+		})
+	}
+}
+
+func TestClientManager_Close(t *testing.T) {
+	cm := &clientManager{
+		isIDL: true,
+		triClients: map[string]*tri.Client{
+			"Method1": tri.NewClient(&http.Client{}, "http://localhost:8080/test1"),
+			"Method2": tri.NewClient(&http.Client{}, "http://localhost:8080/test2"),
+		},
+	}
+
+	err := cm.close()
+	assert.Nil(t, err)
+}
+
+func TestClientManager_CallMethods_MissingClient(t *testing.T) {
+	cm := &clientManager{
+		triClients: map[string]*tri.Client{},
+	}
+	ctx := context.Background()
+
+	t.Run("callUnary missing client", func(t *testing.T) {
+		err := cm.callUnary(ctx, "NonExist", nil, nil)
+		assert.NotNil(t, err)
+		assert.Contains(t, err.Error(), "missing triple client")
+	})
+
+	t.Run("callClientStream missing client", func(t *testing.T) {
+		stream, err := cm.callClientStream(ctx, "NonExist")
+		assert.NotNil(t, err)
+		assert.Nil(t, stream)
+		assert.Contains(t, err.Error(), "missing triple client")
+	})
+
+	t.Run("callServerStream missing client", func(t *testing.T) {
+		stream, err := cm.callServerStream(ctx, "NonExist", nil)
+		assert.NotNil(t, err)
+		assert.Nil(t, stream)
+		assert.Contains(t, err.Error(), "missing triple client")
+	})
+
+	t.Run("callBidiStream missing client", func(t *testing.T) {
+		stream, err := cm.callBidiStream(ctx, "NonExist")
+		assert.NotNil(t, err)
+		assert.Nil(t, stream)
+		assert.Contains(t, err.Error(), "missing triple client")
+	})
+}
+
+func Test_genKeepAliveOptions(t *testing.T) {
+	defaultInterval, _ := time.ParseDuration(constant.DefaultKeepAliveInterval)
+	defaultTimeout, _ := time.ParseDuration(constant.DefaultKeepAliveTimeout)
+
+	tests := []struct {
+		desc           string
+		url            *common.URL
+		tripleConf     *global.TripleConfig
+		expectOptsLen  int
+		expectInterval time.Duration
+		expectTimeout  time.Duration
+		expectErr      bool
+	}{
+		{
+			desc:           "nil triple config",
+			url:            common.NewURLWithOptions(),
+			tripleConf:     nil,
+			expectOptsLen:  2, // readMaxBytes, sendMaxBytes
+			expectInterval: defaultInterval,
+			expectTimeout:  defaultTimeout,
+			expectErr:      false,
+		},
+		{
+			desc: "url with max msg size",
+			url: common.NewURLWithOptions(
+				common.WithParamsValue(constant.MaxCallRecvMsgSize, "10MB"),
+				common.WithParamsValue(constant.MaxCallSendMsgSize, "10MB"),
+			),
+			tripleConf:     nil,
+			expectOptsLen:  2,
+			expectInterval: defaultInterval,
+			expectTimeout:  defaultTimeout,
+			expectErr:      false,
+		},
+		{
+			desc: "url with keepalive params",
+			url: common.NewURLWithOptions(
+				common.WithParamsValue(constant.KeepAliveInterval, "60s"),
+				common.WithParamsValue(constant.KeepAliveTimeout, "20s"),
+			),
+			tripleConf:     nil,
+			expectOptsLen:  2,
+			expectInterval: 60 * time.Second,
+			expectTimeout:  20 * time.Second,
+			expectErr:      false,
+		},
+		{
+			desc: "triple config with keepalive",
+			url:  common.NewURLWithOptions(),
+			tripleConf: &global.TripleConfig{
+				KeepAliveInterval: "45s",
+				KeepAliveTimeout:  "15s",
+			},
+			expectOptsLen:  2,
+			expectInterval: 45 * time.Second,
+			expectTimeout:  15 * time.Second,
+			expectErr:      false,
+		},
+		{
+			desc: "triple config with invalid interval",
+			url:  common.NewURLWithOptions(),
+			tripleConf: &global.TripleConfig{
+				KeepAliveInterval: "invalid",
+			},
+			expectErr: true,
+		},
+		{
+			desc: "triple config with invalid timeout",
+			url:  common.NewURLWithOptions(),
+			tripleConf: &global.TripleConfig{
+				KeepAliveTimeout: "invalid",
+			},
+			expectErr: true,
+		},
+		{
+			desc:           "empty triple config",
+			url:            common.NewURLWithOptions(),
+			tripleConf:     &global.TripleConfig{},
+			expectOptsLen:  2,
+			expectInterval: defaultInterval,
+			expectTimeout:  defaultTimeout,
+			expectErr:      false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			opts, interval, timeout, err := genKeepAliveOptions(test.url, test.tripleConf)
+			if test.expectErr {
+				assert.NotNil(t, err)
+			} else {
+				assert.Nil(t, err)
+				assert.Equal(t, test.expectOptsLen, len(opts))
+				assert.Equal(t, test.expectInterval, interval)
+				assert.Equal(t, test.expectTimeout, timeout)
+			}
+		})
+	}
+}
+
+func Test_newClientManager_Serialization(t *testing.T) {
+	tests := []struct {
+		desc          string
+		serialization string
+		expectIDL     bool
+		expectPanic   bool
+	}{
+		{
+			desc:          "protobuf serialization",
+			serialization: constant.ProtobufSerialization,
+			expectIDL:     true,
+			expectPanic:   false,
+		},
+		{
+			desc:          "json serialization",
+			serialization: constant.JSONSerialization,
+			expectIDL:     true,
+			expectPanic:   false,
+		},
+		{
+			desc:          "hessian2 serialization",
+			serialization: constant.Hessian2Serialization,
+			expectIDL:     false,
+			expectPanic:   false,
+		},
+		{
+			desc:          "msgpack serialization",
+			serialization: constant.MsgpackSerialization,
+			expectIDL:     false,
+			expectPanic:   false,
+		},
+		{
+			desc:          "unsupported serialization",
+			serialization: "unsupported",
+			expectPanic:   true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			url := common.NewURLWithOptions(
+				common.WithLocation("localhost:20000"),
+				common.WithPath("com.example.TestService"),
+				common.WithMethods([]string{"TestMethod"}),
+				common.WithParamsValue(constant.SerializationKey, test.serialization),
+			)
+
+			if test.expectPanic {
+				assert.Panics(t, func() {
+					_, _ = newClientManager(url)
+				})
+			} else {
+				cm, err := newClientManager(url)
+				assert.Nil(t, err)
+				assert.NotNil(t, cm)
+				assert.Equal(t, test.expectIDL, cm.isIDL)
+			}
+		})
+	}
+}
+
+func Test_newClientManager_NoMethods(t *testing.T) {
+	// Test when url has no methods and no RpcServiceKey attribute
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+	)
+
+	cm, err := newClientManager(url)
+	assert.NotNil(t, err)
+	assert.Nil(t, cm)
+	assert.Contains(t, err.Error(), "can't get methods")
+}
+
+func Test_newClientManager_WithMethods(t *testing.T) {
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+		common.WithMethods([]string{"Method1", "Method2", "Method3"}),
+	)
+
+	cm, err := newClientManager(url)
+	assert.Nil(t, err)
+	assert.NotNil(t, cm)
+	assert.Equal(t, 3, len(cm.triClients))
+	assert.Contains(t, cm.triClients, "Method1")
+	assert.Contains(t, cm.triClients, "Method2")
+	assert.Contains(t, cm.triClients, "Method3")
+}
+
+func Test_newClientManager_WithGroupAndVersion(t *testing.T) {
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+		common.WithMethods([]string{"TestMethod"}),
+		common.WithParamsValue(constant.GroupKey, "testGroup"),
+		common.WithParamsValue(constant.VersionKey, "1.0.0"),
+	)
+
+	cm, err := newClientManager(url)
+	assert.Nil(t, err)
+	assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_WithTimeout(t *testing.T) {
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+		common.WithMethods([]string{"TestMethod"}),
+		common.WithParamsValue(constant.TimeoutKey, "5s"),
+	)
+
+	cm, err := newClientManager(url)
+	assert.Nil(t, err)
+	assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_InvalidTLSConfig(t *testing.T) {
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+		common.WithMethods([]string{"TestMethod"}),
+	)
+	// Set invalid TLS config type
+	url.SetAttribute(constant.TLSConfigKey, "invalid-type")
+
+	cm, err := newClientManager(url)
+	assert.NotNil(t, err)
+	assert.Nil(t, cm)
+	assert.Contains(t, err.Error(), "TLSConfig configuration failed")
+}
+
+func Test_newClientManager_HTTP3WithoutTLS(t *testing.T) {
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+		common.WithMethods([]string{"TestMethod"}),
+	)
+	// Enable HTTP/3 without TLS config
+	tripleConfig := &global.TripleConfig{
+		Http3: &global.Http3Config{
+			Enable: true,
+		},
+	}
+	url.SetAttribute(constant.TripleConfigKey, tripleConfig)
+
+	cm, err := newClientManager(url)
+	assert.NotNil(t, err)
+	assert.Nil(t, cm)
+	assert.Contains(t, err.Error(), "must have TLS config")
+}
+
+// mockService is a mock service for testing reflection-based client creation
+type mockService struct{}
+
+func (m *mockService) Reference() string {
+	return "mockService"
+}
+
+func (m *mockService) TestMethod1(ctx context.Context, req string) (string, error) {
+	return req, nil
+}
+
+func (m *mockService) TestMethod2(ctx context.Context, req int) (int, error) {
+	return req, nil
+}
+
+func Test_newClientManager_WithRpcService(t *testing.T) {
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+		// No methods specified, will use reflection
+	)
+	url.SetAttribute(constant.RpcServiceKey, &mockService{})
+
+	cm, err := newClientManager(url)
+	assert.Nil(t, err)
+	assert.NotNil(t, cm)
+	// Should have methods from mockService (Reference, TestMethod1, TestMethod2)
+	assert.True(t, len(cm.triClients) >= 2)
+}
+
+func TestDualTransport_Structure(t *testing.T) {
+	keepAliveInterval := 30 * time.Second
+	keepAliveTimeout := 5 * time.Second
+
+	transport := newDualTransport(nil, keepAliveInterval, keepAliveTimeout)
+	assert.NotNil(t, transport)
+
+	dt, ok := transport.(*dualTransport)
+	assert.True(t, ok)
+	assert.NotNil(t, dt.http2Transport)
+	assert.NotNil(t, dt.http3Transport)
+	assert.NotNil(t, dt.altSvcCache)
+}
+
+func Test_newClientManager_HTTP2WithTLS(t *testing.T) {
+	// This test requires valid TLS config files
+	// Skip if files don't exist
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+		common.WithMethods([]string{"TestMethod"}),
+	)
+
+	// Set a valid TLS config structure but with non-existent files
+	// This will test the TLS config parsing path
+	tlsConfig := &global.TLSConfig{
+		CACertFile:  "non-existent-ca.crt",
+		TLSCertFile: "non-existent-server.crt",
+		TLSKeyFile:  "non-existent-server.key",
+	}
+	url.SetAttribute(constant.TLSConfigKey, tlsConfig)
+
+	// Should fail due to missing cert files, but tests the TLS path
+	cm, err := newClientManager(url)
+	// Either succeeds (if TLS validation is lenient) or fails with TLS error
+	if err != nil {
+		// Expected - TLS files don't exist
+		t.Logf("Expected TLS error: %v", err)
+	} else {
+		assert.NotNil(t, cm)
+	}
+}
+
+func Test_newClientManager_URLPrefixHandling(t *testing.T) {
+	tests := []struct {
+		desc     string
+		location string
+	}{
+		{
+			desc:     "location without prefix",
+			location: "localhost:20000",
+		},
+		{
+			desc:     "location with http prefix",
+			location: "http://localhost:20000",
+		},
+		{
+			desc:     "location with https prefix",
+			location: "https://localhost:20000",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			url := common.NewURLWithOptions(
+				common.WithLocation(test.location),
+				common.WithPath("com.example.TestService"),
+				common.WithMethods([]string{"TestMethod"}),
+			)
+
+			cm, err := newClientManager(url)
+			assert.Nil(t, err)
+			assert.NotNil(t, cm)
+			assert.Equal(t, 1, len(cm.triClients))
+		})
+	}
+}
+
+func Test_newClientManager_KeepAliveError(t *testing.T) {
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+		common.WithMethods([]string{"TestMethod"}),
+	)
+
+	// Set triple config with invalid keepalive that will cause error
+	tripleConfig := &global.TripleConfig{
+		KeepAliveInterval: "invalid-duration",
+	}
+	url.SetAttribute(constant.TripleConfigKey, tripleConfig)
+
+	cm, err := newClientManager(url)
+	assert.NotNil(t, err)
+	assert.Nil(t, cm)
+}
+
+func Test_newClientManager_DefaultProtocol(t *testing.T) {
+	// Test default HTTP/2 protocol selection
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+		common.WithMethods([]string{"TestMethod"}),
+	)
+
+	cm, err := newClientManager(url)
+	assert.Nil(t, err)
+	assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_EmptyTripleConfig(t *testing.T) {
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+		common.WithMethods([]string{"TestMethod"}),
+	)
+
+	// Set empty triple config (Http3 is nil)
+	tripleConfig := &global.TripleConfig{}
+	url.SetAttribute(constant.TripleConfigKey, tripleConfig)
+
+	cm, err := newClientManager(url)
+	assert.Nil(t, err)
+	assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_Http3Disabled(t *testing.T) {
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+		common.WithMethods([]string{"TestMethod"}),
+	)
+
+	// Set triple config with Http3 disabled
+	tripleConfig := &global.TripleConfig{
+		Http3: &global.Http3Config{
+			Enable: false,
+		},
+	}
+	url.SetAttribute(constant.TripleConfigKey, tripleConfig)
+
+	cm, err := newClientManager(url)
+	assert.Nil(t, err)
+	assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_MultipleMethods(t *testing.T) {
+	methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+		common.WithMethods(methods),
+	)
+
+	cm, err := newClientManager(url)
+	assert.Nil(t, err)
+	assert.NotNil(t, cm)
+	assert.Equal(t, len(methods), len(cm.triClients))
+
+	for _, method := range methods {
+		_, exists := cm.triClients[method]
+		assert.True(t, exists, "method %s should exist", method)
+	}
+}
+
+func Test_newClientManager_InterfaceName(t *testing.T) {
+	url := common.NewURLWithOptions(
+		common.WithLocation("localhost:20000"),
+		common.WithPath("com.example.TestService"),
+		common.WithInterface("com.example.ITestService"),
+		common.WithMethods([]string{"TestMethod"}),
+	)
+
+	cm, err := newClientManager(url)
+	assert.Nil(t, err)
+	assert.NotNil(t, cm)
+}
diff --git a/protocol/triple/server_test.go b/protocol/triple/server_test.go
index c6b705141..d6129fea6 100644
--- a/protocol/triple/server_test.go
+++ b/protocol/triple/server_test.go
@@ -18,15 +18,21 @@
 package triple
 
 import (
+	"context"
+	"fmt"
 	"net/http"
+	"sync"
 	"testing"
 )
 
 import (
 	"github.com/stretchr/testify/assert"
+
+	"google.golang.org/grpc"
 )
 
 import (
+	"dubbo.apache.org/dubbo-go/v3/common"
 	"dubbo.apache.org/dubbo-go/v3/common/constant"
 	"dubbo.apache.org/dubbo-go/v3/global"
 )
@@ -135,7 +141,7 @@ func TestServer_ProtocolSelection(t *testing.T) {
 
 			// Extract the protocol selection logic for testing
 			var callProtocol string
-			if tripleConfig != nil && tripleConfig.Http3 != nil && tripleConfig.Http3.Enable {
+			if tripleConfig.Http3 != nil && tripleConfig.Http3.Enable {
 				callProtocol = constant.CallHTTP2AndHTTP3
 			} else {
 				callProtocol = constant.CallHTTP2
@@ -145,3 +151,338 @@ func TestServer_ProtocolSelection(t *testing.T) {
 		})
 	}
 }
+
+func TestNewServer(t *testing.T) {
+	tests := []struct {
+		desc string
+		cfg  *global.TripleConfig
+	}{
+		{
+			desc: "nil config",
+			cfg:  nil,
+		},
+		{
+			desc: "empty config",
+			cfg:  &global.TripleConfig{},
+		},
+		{
+			desc: "config with http3",
+			cfg: &global.TripleConfig{
+				Http3: &global.Http3Config{
+					Enable: true,
+				},
+			},
+		},
+		{
+			desc: "config with keepalive",
+			cfg: &global.TripleConfig{
+				KeepAliveInterval: "30s",
+				KeepAliveTimeout:  "10s",
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			server := NewServer(test.cfg)
+			assert.NotNil(t, server)
+			assert.Equal(t, test.cfg, server.cfg)
+			assert.NotNil(t, server.services)
+			assert.Empty(t, server.services)
+		})
+	}
+}
+
+func TestServer_GetServiceInfo(t *testing.T) {
+	t.Run("empty services", func(t *testing.T) {
+		server := NewServer(nil)
+		info := server.GetServiceInfo()
+		assert.NotNil(t, info)
+		assert.Empty(t, info)
+	})
+
+	t.Run("with services", func(t *testing.T) {
+		server := NewServer(nil)
+		// manually add service info for testing
+		server.mu.Lock()
+		server.services["test.Service"] = grpc.ServiceInfo{
+			Methods: []grpc.MethodInfo{
+				{Name: "Method1", IsClientStream: false, IsServerStream: false},
+			},
+		}
+		server.mu.Unlock()
+
+		info := server.GetServiceInfo()
+		assert.NotNil(t, info)
+		assert.Equal(t, 1, len(info))
+		assert.Contains(t, info, "test.Service")
+	})
+
+	t.Run("returns copy not reference", func(t *testing.T) {
+		server := NewServer(nil)
+		server.mu.Lock()
+		server.services["test.Service"] = grpc.ServiceInfo{}
+		server.mu.Unlock()
+
+		info1 := server.GetServiceInfo()
+		info2 := server.GetServiceInfo()
+
+		// modify info1 should not affect info2
+		delete(info1, "test.Service")
+		assert.Contains(t, info2, "test.Service")
+	})
+}
+
+func TestServer_SaveServiceInfo(t *testing.T) {
+	tests := []struct {
+		desc          string
+		interfaceName string
+		info          *common.ServiceInfo
+		expect        func(t *testing.T, server *Server)
+	}{
+		{
+			desc:          "unary method",
+			interfaceName: "test.UnaryService",
+			info: &common.ServiceInfo{
+				Methods: []common.MethodInfo{
+					{Name: "UnaryMethod", Type: constant.CallUnary},
+				},
+			},
+			expect: func(t *testing.T, server *Server) {
+				info := server.GetServiceInfo()
+				assert.Contains(t, info, "test.UnaryService")
+				assert.Equal(t, 1, len(info["test.UnaryService"].Methods))
+				assert.Equal(t, "UnaryMethod", info["test.UnaryService"].Methods[0].Name)
+				assert.False(t, info["test.UnaryService"].Methods[0].IsClientStream)
+				assert.False(t, info["test.UnaryService"].Methods[0].IsServerStream)
+			},
+		},
+		{
+			desc:          "client stream method",
+			interfaceName: "test.ClientStreamService",
+			info: &common.ServiceInfo{
+				Methods: []common.MethodInfo{
+					{Name: "ClientStreamMethod", Type: constant.CallClientStream},
+				},
+			},
+			expect: func(t *testing.T, server *Server) {
+				info := server.GetServiceInfo()
+				assert.Contains(t, info, "test.ClientStreamService")
+				assert.True(t, info["test.ClientStreamService"].Methods[0].IsClientStream)
+				assert.False(t, info["test.ClientStreamService"].Methods[0].IsServerStream)
+			},
+		},
+		{
+			desc:          "server stream method",
+			interfaceName: "test.ServerStreamService",
+			info: &common.ServiceInfo{
+				Methods: []common.MethodInfo{
+					{Name: "ServerStreamMethod", Type: constant.CallServerStream},
+				},
+			},
+			expect: func(t *testing.T, server *Server) {
+				info := server.GetServiceInfo()
+				assert.Contains(t, info, "test.ServerStreamService")
+				assert.False(t, info["test.ServerStreamService"].Methods[0].IsClientStream)
+				assert.True(t, info["test.ServerStreamService"].Methods[0].IsServerStream)
+			},
+		},
+		{
+			desc:          "bidi stream method",
+			interfaceName: "test.BidiStreamService",
+			info: &common.ServiceInfo{
+				Methods: []common.MethodInfo{
+					{Name: "BidiStreamMethod", Type: constant.CallBidiStream},
+				},
+			},
+			expect: func(t *testing.T, server *Server) {
+				info := server.GetServiceInfo()
+				assert.Contains(t, info, "test.BidiStreamService")
+				assert.True(t, info["test.BidiStreamService"].Methods[0].IsClientStream)
+				assert.True(t, info["test.BidiStreamService"].Methods[0].IsServerStream)
+			},
+		},
+		{
+			desc:          "multiple methods",
+			interfaceName: "test.MultiMethodService",
+			info: &common.ServiceInfo{
+				Methods: []common.MethodInfo{
+					{Name: "Method1", Type: constant.CallUnary},
+					{Name: "Method2", Type: constant.CallClientStream},
+					{Name: "Method3", Type: constant.CallServerStream},
+					{Name: "Method4", Type: constant.CallBidiStream},
+				},
+			},
+			expect: func(t *testing.T, server *Server) {
+				info := server.GetServiceInfo()
+				assert.Contains(t, info, "test.MultiMethodService")
+				assert.Equal(t, 4, len(info["test.MultiMethodService"].Methods))
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			server := NewServer(nil)
+			server.saveServiceInfo(test.interfaceName, test.info)
+			test.expect(t, server)
+		})
+	}
+}
+
+func TestServer_SaveServiceInfo_Concurrent(t *testing.T) {
+	server := NewServer(nil)
+	var wg sync.WaitGroup
+	concurrency := 10
+
+	for i := 0; i < concurrency; i++ {
+		wg.Add(1)
+		go func(idx int) {
+			defer wg.Done()
+			info := &common.ServiceInfo{
+				Methods: []common.MethodInfo{
+					{Name: "Method", Type: constant.CallUnary},
+				},
+			}
+			server.saveServiceInfo(fmt.Sprintf("test.Service%d", idx), info)
+		}(i)
+	}
+
+	wg.Wait()
+	assert.Equal(t, concurrency, len(server.GetServiceInfo()))
+}
+
+func Test_getHanOpts(t *testing.T) {
+	tests := []struct {
+		desc       string
+		url        *common.URL
+		tripleConf *global.TripleConfig
+		expectLen  int
+	}{
+		{
+			desc:       "basic url without triple config",
+			url:        common.NewURLWithOptions(),
+			tripleConf: nil,
+			expectLen:  4, // group, version, readMaxBytes, sendMaxBytes
+		},
+		{
+			desc: "url with group and version",
+			url: common.NewURLWithOptions(
+				common.WithParamsValue(constant.GroupKey, "testGroup"),
+				common.WithParamsValue(constant.VersionKey, "1.0.0"),
+			),
+			tripleConf: nil,
+			expectLen:  4,
+		},
+		{
+			desc: "url with max msg size",
+			url: common.NewURLWithOptions(
+				common.WithParamsValue(constant.MaxServerRecvMsgSize, "10MB"),
+				common.WithParamsValue(constant.MaxServerSendMsgSize, "10MB"),
+			),
+			tripleConf: nil,
+			expectLen:  4,
+		},
+		{
+			desc: "with triple config max msg size",
+			url:  common.NewURLWithOptions(),
+			tripleConf: &global.TripleConfig{
+				MaxServerRecvMsgSize: "20MB",
+				MaxServerSendMsgSize: "20MB",
+			},
+			expectLen: 6, // base 4 + 2 from tripleConf
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			opts := getHanOpts(test.url, test.tripleConf)
+			assert.Equal(t, test.expectLen, len(opts))
+		})
+	}
+}
+
+// mockRPCService is a mock service for testing createServiceInfoWithReflection
+type mockRPCService struct{}
+
+func (m *mockRPCService) Reference() string {
+	return "mockRPCService"
+}
+
+func (m *mockRPCService) TestMethod(ctx context.Context, req string) (string, error) {
+	return req, nil
+}
+
+func (m *mockRPCService) TestMethodWithMultipleArgs(ctx context.Context, arg1 string, arg2 int) (string, error) {
+	return arg1, nil
+}
+
+func Test_createServiceInfoWithReflection(t *testing.T) {
+	t.Run("basic service", func(t *testing.T) {
+		svc := &mockRPCService{}
+		info := createServiceInfoWithReflection(svc)
+
+		assert.NotNil(t, info)
+		assert.NotEmpty(t, info.Methods)
+
+		// should have TestMethod, TestMethodWithMultipleArgs, and $invoke
+		methodNames := make([]string, 0)
+		for _, m := range info.Methods {
+			methodNames = append(methodNames, m.Name)
+		}
+		assert.Contains(t, methodNames, "TestMethod")
+		assert.Contains(t, methodNames, "TestMethodWithMultipleArgs")
+		assert.Contains(t, methodNames, "$invoke") // generic call method
+	})
+
+	t.Run("method type is CallUnary", func(t *testing.T) {
+		svc := &mockRPCService{}
+		info := createServiceInfoWithReflection(svc)
+
+		for _, m := range info.Methods {
+			assert.Equal(t, constant.CallUnary, m.Type)
+		}
+	})
+
+	t.Run("ReqInitFunc returns correct params", func(t *testing.T) {
+		svc := &mockRPCService{}
+		info := createServiceInfoWithReflection(svc)
+
+		for _, m := range info.Methods {
+			if m.Name == "TestMethod" {
+				params := m.ReqInitFunc()
+				assert.NotNil(t, params)
+				paramsSlice, ok := params.([]any)
+				assert.True(t, ok)
+				assert.Equal(t, 1, len(paramsSlice)) // only req param (ctx is excluded)
+			}
+			if m.Name == "TestMethodWithMultipleArgs" {
+				params := m.ReqInitFunc()
+				paramsSlice, ok := params.([]any)
+				assert.True(t, ok)
+				assert.Equal(t, 2, len(paramsSlice)) // arg1 and arg2
+			}
+		}
+	})
+
+	t.Run("generic invoke method", func(t *testing.T) {
+		svc := &mockRPCService{}
+		info := createServiceInfoWithReflection(svc)
+
+		var invokeMethod *common.MethodInfo
+		for i := range info.Methods {
+			if info.Methods[i].Name == "$invoke" {
+				invokeMethod = &info.Methods[i]
+				break
+			}
+		}
+
+		assert.NotNil(t, invokeMethod)
+		assert.Equal(t, constant.CallUnary, invokeMethod.Type)
+
+		params := invokeMethod.ReqInitFunc()
+		paramsSlice, ok := params.([]any)
+		assert.True(t, ok)
+		assert.Equal(t, 3, len(paramsSlice)) // methodName, argv types, argv
+	})
+}
diff --git a/protocol/triple/triple_invoker_test.go b/protocol/triple/triple_invoker_test.go
index 09788c450..9f8575aaa 100644
--- a/protocol/triple/triple_invoker_test.go
+++ b/protocol/triple/triple_invoker_test.go
@@ -20,6 +20,7 @@ package triple
 import (
 	"context"
 	"net/http"
+	"sync"
 	"testing"
 )
 
@@ -54,6 +55,7 @@ func Test_parseInvocation(t *testing.T) {
 			},
 			expect: func(t *testing.T, callType string, inRaw []any, methodName string, err error) {
 				assert.NotNil(t, err)
+				assert.Contains(t, err.Error(), "miss CallType")
 			},
 		},
 		{
@@ -69,6 +71,7 @@ func Test_parseInvocation(t *testing.T) {
 			},
 			expect: func(t *testing.T, callType string, inRaw []any, methodName string, err error) {
 				assert.NotNil(t, err)
+				assert.Contains(t, err.Error(), "CallType should be string")
 			},
 		},
 		{
@@ -84,6 +87,87 @@ func Test_parseInvocation(t *testing.T) {
 			},
 			expect: func(t *testing.T, callType string, inRaw []any, methodName string, err error) {
 				assert.NotNil(t, err)
+				assert.Contains(t, err.Error(), "miss MethodName")
+			},
+		},
+		{
+			desc: "success with CallUnary",
+			ctx: func() context.Context {
+				return context.Background()
+			},
+			url: common.NewURLWithOptions(),
+			invo: func() base.Invocation {
+				iv := invocation.NewRPCInvocationWithOptions(
+					invocation.WithMethodName("TestMethod"),
+					invocation.WithParameterRawValues([]any{"req", "resp"}),
+				)
+				iv.SetAttribute(constant.CallTypeKey, constant.CallUnary)
+				return iv
+			},
+			expect: func(t *testing.T, callType string, inRaw []any, methodName string, err error) {
+				assert.Nil(t, err)
+				assert.Equal(t, constant.CallUnary, callType)
+				assert.Equal(t, "TestMethod", methodName)
+				assert.Equal(t, 2, len(inRaw))
+			},
+		},
+		{
+			desc: "success with CallClientStream",
+			ctx: func() context.Context {
+				return context.Background()
+			},
+			url: common.NewURLWithOptions(),
+			invo: func() base.Invocation {
+				iv := invocation.NewRPCInvocationWithOptions(
+					invocation.WithMethodName("StreamMethod"),
+				)
+				iv.SetAttribute(constant.CallTypeKey, constant.CallClientStream)
+				return iv
+			},
+			expect: func(t *testing.T, callType string, inRaw []any, methodName string, err error) {
+				assert.Nil(t, err)
+				assert.Equal(t, constant.CallClientStream, callType)
+				assert.Equal(t, "StreamMethod", methodName)
+			},
+		},
+		{
+			desc: "success with CallServerStream",
+			ctx: func() context.Context {
+				return context.Background()
+			},
+			url: common.NewURLWithOptions(),
+			invo: func() base.Invocation {
+				iv := invocation.NewRPCInvocationWithOptions(
+					invocation.WithMethodName("ServerStreamMethod"),
+					invocation.WithParameterRawValues([]any{"req"}),
+				)
+				iv.SetAttribute(constant.CallTypeKey, constant.CallServerStream)
+				return iv
+			},
+			expect: func(t *testing.T, callType string, inRaw []any, methodName string, err error) {
+				assert.Nil(t, err)
+				assert.Equal(t, constant.CallServerStream, callType)
+				assert.Equal(t, "ServerStreamMethod", methodName)
+				assert.Equal(t, 1, len(inRaw))
+			},
+		},
+		{
+			desc: "success with CallBidiStream",
+			ctx: func() context.Context {
+				return context.Background()
+			},
+			url: common.NewURLWithOptions(),
+			invo: func() base.Invocation {
+				iv := invocation.NewRPCInvocationWithOptions(
+					invocation.WithMethodName("BidiStreamMethod"),
+				)
+				iv.SetAttribute(constant.CallTypeKey, constant.CallBidiStream)
+				return iv
+			},
+			expect: func(t *testing.T, callType string, inRaw []any, methodName string, err error) {
+				assert.Nil(t, err)
+				assert.Equal(t, constant.CallBidiStream, callType)
+				assert.Equal(t, "BidiStreamMethod", methodName)
 			},
 		},
 	}
@@ -157,6 +241,52 @@ func Test_parseAttachments(t *testing.T) {
 			},
 			expect: func(t *testing.T, ctx context.Context, err error) {
 				assert.NotNil(t, err)
+				assert.Contains(t, err.Error(), "invalid")
+			},
+		},
+		{
+			desc: "url has timeout key",
+			ctx: func() context.Context {
+				return context.Background()
+			},
+			url: common.NewURLWithOptions(
+				common.WithParamsValue(constant.TimeoutKey, "3000"),
+			),
+			invo: func() base.Invocation {
+				return invocation.NewRPCInvocationWithOptions()
+			},
+			expect: func(t *testing.T, ctx context.Context, err error) {
+				assert.Nil(t, err)
+				header := http.Header(tri.ExtractFromOutgoingContext(ctx))
+				assert.NotNil(t, header)
+				assert.Equal(t, "3000", header.Get(constant.TimeoutKey))
+			},
+		},
+		{
+			desc: "empty context attachments",
+			ctx: func() context.Context {
+				return context.Background()
+			},
+			url: common.NewURLWithOptions(),
+			invo: func() base.Invocation {
+				return invocation.NewRPCInvocationWithOptions()
+			},
+			expect: func(t *testing.T, ctx context.Context, err error) {
+				assert.Nil(t, err)
+			},
+		},
+		{
+			desc: "context with non-map attachment value",
+			ctx: func() context.Context {
+				return context.WithValue(context.Background(), constant.AttachmentKey, "not-a-map")
+			},
+			url: common.NewURLWithOptions(),
+			invo: func() base.Invocation {
+				return invocation.NewRPCInvocationWithOptions()
+			},
+			expect: func(t *testing.T, ctx context.Context, err error) {
+				// non-map attachment should be ignored
+				assert.Nil(t, err)
 			},
 		},
 	}
@@ -171,3 +301,329 @@ func Test_parseAttachments(t *testing.T) {
 		})
 	}
 }
+
+// newTestTripleInvoker creates a TripleInvoker for testing without network connection
+func newTestTripleInvoker(url *common.URL, cm *clientManager) *TripleInvoker {
+	return &TripleInvoker{
+		BaseInvoker:   *base.NewBaseInvoker(url),
+		quitOnce:      sync.Once{},
+		clientGuard:   &sync.RWMutex{},
+		clientManager: cm,
+	}
+}
+
+func TestTripleInvoker_SetGetClientManager(t *testing.T) {
+	url := common.NewURLWithOptions()
+	ti := newTestTripleInvoker(url, nil)
+
+	// initially nil
+	assert.Nil(t, ti.getClientManager())
+
+	// set clientManager
+	cm := &clientManager{
+		isIDL:      true,
+		triClients: make(map[string]*tri.Client),
+	}
+	ti.setClientManager(cm)
+	assert.Equal(t, cm, ti.getClientManager())
+
+	// set to nil
+	ti.setClientManager(nil)
+	assert.Nil(t, ti.getClientManager())
+}
+
+func TestTripleInvoker_IsAvailable(t *testing.T) {
+	tests := []struct {
+		desc          string
+		clientManager *clientManager
+		expect        bool
+	}{
+		{
+			desc:          "clientManager is nil",
+			clientManager: nil,
+			expect:        false,
+		},
+		{
+			desc: "clientManager is not nil",
+			clientManager: &clientManager{
+				isIDL:      true,
+				triClients: make(map[string]*tri.Client),
+			},
+			expect: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			url := common.NewURLWithOptions()
+			ti := newTestTripleInvoker(url, test.clientManager)
+			assert.Equal(t, test.expect, ti.IsAvailable())
+		})
+	}
+}
+
+func TestTripleInvoker_IsDestroyed(t *testing.T) {
+	tests := []struct {
+		desc          string
+		clientManager *clientManager
+		destroyed     bool
+		expect        bool
+	}{
+		{
+			desc:          "clientManager is nil",
+			clientManager: nil,
+			destroyed:     false,
+			expect:        false,
+		},
+		{
+			desc: "clientManager is not nil and not destroyed",
+			clientManager: &clientManager{
+				isIDL:      true,
+				triClients: make(map[string]*tri.Client),
+			},
+			destroyed: false,
+			expect:    false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			url := common.NewURLWithOptions()
+			ti := newTestTripleInvoker(url, test.clientManager)
+			assert.Equal(t, test.expect, ti.IsDestroyed())
+		})
+	}
+}
+
+func TestTripleInvoker_Destroy(t *testing.T) {
+	t.Run("destroy with clientManager", func(t *testing.T) {
+		url := common.NewURLWithOptions()
+		cm := &clientManager{
+			isIDL:      true,
+			triClients: make(map[string]*tri.Client),
+		}
+		ti := newTestTripleInvoker(url, cm)
+
+		assert.True(t, ti.IsAvailable())
+		assert.NotNil(t, ti.getClientManager())
+
+		ti.Destroy()
+
+		assert.False(t, ti.IsAvailable())
+		assert.Nil(t, ti.getClientManager())
+	})
+
+	t.Run("destroy without clientManager", func(t *testing.T) {
+		url := common.NewURLWithOptions()
+		ti := newTestTripleInvoker(url, nil)
+
+		ti.Destroy()
+
+		assert.False(t, ti.IsAvailable())
+		assert.Nil(t, ti.getClientManager())
+	})
+
+	t.Run("destroy called multiple times", func(t *testing.T) {
+		url := common.NewURLWithOptions()
+		cm := &clientManager{
+			isIDL:      true,
+			triClients: make(map[string]*tri.Client),
+		}
+		ti := newTestTripleInvoker(url, cm)
+
+		// first destroy
+		ti.Destroy()
+		assert.Nil(t, ti.getClientManager())
+
+		// second destroy should not panic
+		ti.Destroy()
+		assert.Nil(t, ti.getClientManager())
+	})
+}
+
+func TestTripleInvoker_Invoke(t *testing.T) {
+	tests := []struct {
+		desc         string
+		setup        func() (*TripleInvoker, base.Invocation)
+		expectErr    error
+		expectErrMsg string
+	}{
+		{
+			desc: "invoker is destroyed",
+			setup: func() (*TripleInvoker, base.Invocation) {
+				url := common.NewURLWithOptions()
+				ti := newTestTripleInvoker(url, &clientManager{
+					isIDL:      true,
+					triClients: make(map[string]*tri.Client),
+				})
+				ti.Destroy()
+				inv := invocation.NewRPCInvocationWithOptions()
+				return ti, inv
+			},
+			expectErr: base.ErrDestroyedInvoker,
+		},
+		{
+			desc: "clientManager is nil",
+			setup: func() (*TripleInvoker, base.Invocation) {
+				url := common.NewURLWithOptions()
+				ti := newTestTripleInvoker(url, nil)
+				inv := invocation.NewRPCInvocationWithOptions()
+				return ti, inv
+			},
+			expectErr: base.ErrClientClosed,
+		},
+		{
+			desc: "parseInvocation error - miss callType",
+			setup: func() (*TripleInvoker, base.Invocation) {
+				url := common.NewURLWithOptions()
+				ti := newTestTripleInvoker(url, &clientManager{
+					isIDL:      true,
+					triClients: make(map[string]*tri.Client),
+				})
+				inv := invocation.NewRPCInvocationWithOptions()
+				return ti, inv
+			},
+			expectErrMsg: "miss CallType",
+		},
+		{
+			desc: "mergeAttachmentToOutgoing error - invalid attachment",
+			setup: func() (*TripleInvoker, base.Invocation) {
+				url := common.NewURLWithOptions()
+				ti := newTestTripleInvoker(url, &clientManager{
+					isIDL:      true,
+					triClients: make(map[string]*tri.Client),
+				})
+				inv := invocation.NewRPCInvocationWithOptions(
+					invocation.WithMethodName("TestMethod"),
+					invocation.WithAttachment("invalid_key", 123), // invalid attachment type
+				)
+				inv.SetAttribute(constant.CallTypeKey, constant.CallUnary)
+				return ti, inv
+			},
+			expectErrMsg: "invalid",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ti, inv := test.setup()
+			result := ti.Invoke(context.Background(), inv)
+			if test.expectErr != nil {
+				assert.Equal(t, test.expectErr, result.Error())
+			}
+			if test.expectErrMsg != "" {
+				assert.NotNil(t, result.Error())
+				assert.Contains(t, result.Error().Error(), test.expectErrMsg)
+			}
+		})
+	}
+}
+
+func TestTripleInvoker_Invoke_Concurrent(t *testing.T) {
+	url := common.NewURLWithOptions()
+	ti := newTestTripleInvoker(url, &clientManager{
+		isIDL:      true,
+		triClients: make(map[string]*tri.Client),
+	})
+
+	var wg sync.WaitGroup
+	concurrency := 10
+
+	for i := 0; i < concurrency; i++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			inv := invocation.NewRPCInvocationWithOptions()
+			_ = ti.Invoke(context.Background(), inv)
+		}()
+	}
+
+	wg.Wait()
+}
+
+func Test_mergeAttachmentToOutgoing(t *testing.T) {
+	tests := []struct {
+		desc   string
+		ctx    context.Context
+		invo   func() base.Invocation
+		expect func(t *testing.T, ctx context.Context, err error)
+	}{
+		{
+			desc: "with timeout attachment",
+			ctx:  context.Background(),
+			invo: func() base.Invocation {
+				inv := invocation.NewRPCInvocationWithOptions(
+					invocation.WithAttachment(constant.TimeoutKey, "5000"),
+				)
+				return inv
+			},
+			expect: func(t *testing.T, ctx context.Context, err error) {
+				assert.Nil(t, err)
+				timeout := ctx.Value(tri.TimeoutKey{})
+				assert.Equal(t, "5000", timeout)
+			},
+		},
+		{
+			desc: "with string attachment",
+			ctx:  context.Background(),
+			invo: func() base.Invocation {
+				inv := invocation.NewRPCInvocationWithOptions(
+					invocation.WithAttachment("custom-key", "custom-value"),
+				)
+				return inv
+			},
+			expect: func(t *testing.T, ctx context.Context, err error) {
+				assert.Nil(t, err)
+				header := http.Header(tri.ExtractFromOutgoingContext(ctx))
+				assert.Equal(t, "custom-value", header.Get("custom-key"))
+			},
+		},
+		{
+			desc: "with string slice attachment",
+			ctx:  context.Background(),
+			invo: func() base.Invocation {
+				inv := invocation.NewRPCInvocationWithOptions(
+					invocation.WithAttachment("multi-key", []string{"val1", "val2"}),
+				)
+				return inv
+			},
+			expect: func(t *testing.T, ctx context.Context, err error) {
+				assert.Nil(t, err)
+				header := http.Header(tri.ExtractFromOutgoingContext(ctx))
+				assert.Equal(t, []string{"val1", "val2"}, header.Values("multi-key"))
+			},
+		},
+		{
+			desc: "with invalid attachment type",
+			ctx:  context.Background(),
+			invo: func() base.Invocation {
+				inv := invocation.NewRPCInvocationWithOptions(
+					invocation.WithAttachment("invalid-key", 12345),
+				)
+				return inv
+			},
+			expect: func(t *testing.T, ctx context.Context, err error) {
+				assert.NotNil(t, err)
+				assert.Contains(t, err.Error(), "invalid")
+			},
+		},
+		{
+			desc: "with empty attachments",
+			ctx:  context.Background(),
+			invo: func() base.Invocation {
+				return invocation.NewRPCInvocationWithOptions()
+			},
+			expect: func(t *testing.T, ctx context.Context, err error) {
+				assert.Nil(t, err)
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			inv := test.invo()
+			ctx, err := mergeAttachmentToOutgoing(test.ctx, inv)
+			test.expect(t, ctx, err)
+		})
+	}
+}
diff --git a/protocol/triple/triple_test.go b/protocol/triple/triple_test.go
new file mode 100644
index 000000000..80c73aa21
--- /dev/null
+++ b/protocol/triple/triple_test.go
@@ -0,0 +1,69 @@
+/*
+ * 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 triple
+
+import (
+	"testing"
+)
+
+import (
+	"github.com/stretchr/testify/assert"
+)
+
+import (
+	"dubbo.apache.org/dubbo-go/v3/common/extension"
+)
+
+func TestNewTripleProtocol(t *testing.T) {
+	tp := NewTripleProtocol()
+
+	assert.NotNil(t, tp)
+	assert.NotNil(t, tp.serverMap)
+	assert.Empty(t, tp.serverMap)
+}
+
+func TestGetProtocol(t *testing.T) {
+	// reset singleton for test isolation
+	tripleProtocol = nil
+
+	p1 := GetProtocol()
+	assert.NotNil(t, p1)
+
+	// should return same instance (singleton)
+	p2 := GetProtocol()
+	assert.Same(t, p1, p2)
+}
+
+func TestTripleProtocolRegistration(t *testing.T) {
+	// verify protocol is registered via init()
+	p := extension.GetProtocol(TRIPLE)
+	assert.NotNil(t, p)
+}
+
+func TestTripleConstant(t *testing.T) {
+	assert.Equal(t, "tri", TRIPLE)
+}
+
+func TestTripleProtocol_Destroy_EmptyServerMap(t *testing.T) {
+	tp := NewTripleProtocol()
+
+	// should not panic when serverMap is empty
+	assert.NotPanics(t, func() {
+		tp.Destroy()
+	})
+}