Repository: gavv/httpexpect
Pull request #81 (2020-08-10 10:59:51)

Add WithHost feature.
feature: #66 
1. Add WithHost method to *Request.
2. Add TestRequestWithHost.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/32645077/badge)](https://coveralls.io/builds/32645077)

Coverage decreased (-0.06%) to 93.897% when pulling **7943f0a416b98fb159e94d9070b0efe04c122b71 on allerria:add-with-host-feature** into **3dd83fcd5f57a3fe3c8b2674b2ec2c3475301afa on gavv:master**.


Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 0000000..ca8eed1
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,58 @@
+name: build
+
+on:
+  pull_request:
+    branches:
+      - master
+      - v*
+
+  push:
+    branches:
+      - master
+      - v*
+    tags:
+      - v*
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        go: [ '1.14', '1.15' ]
+
+    name: Go ${{ matrix.go }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Install Go
+        uses: actions/setup-go@v1
+        with:
+          go-version: ${{ matrix.go }}
+
+      - name: Build
+        run: go get -v .
+
+      - name: Run tests
+        run: go test -covermode=count -coverprofile=coverage.out
+
+      - name: Run linters
+        uses: golangci/golangci-lint-action@v2
+        with:
+          version: v1.28
+
+      - name: Build examples
+        run: cd _examples && go get -v .
+
+      - name: Run examples tests
+        run: cd _examples && go test
+
+      - name: Prepare coverage report
+        uses: jandelgado/gcov2lcov-action@v1.0.5
+
+      - name: Send coverage report
+        uses: coverallsapp/github-action@master
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          path-to-lcov: coverage.lcov
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index eaea0e1..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-language: go
-
-go:
-  - 1.14.x
-  - 1.x
-
-env:
-  - GO111MODULE=on
-
-before_install:
-  - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0
-  - go get golang.org/x/tools/cmd/cover
-  - go get github.com/mattn/goveralls
-
-script:
-  - go get -v .
-  - go test -coverprofile profile.cov
-  - ${GOPATH}/bin/golangci-lint run .
-
-  - ( cd _examples && go get -v . )
-  - ( cd _examples && go test )
-
-  - ${GOPATH}/bin/goveralls -coverprofile profile.cov -service=travis-ci
diff --git a/README.md b/README.md
index 0856653..7a8b343 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# httpexpect [![GoDev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/gavv/httpexpect) [![Travis](https://img.shields.io/travis/gavv/httpexpect.svg)](https://travis-ci.org/gavv/httpexpect) [![Coveralls](https://coveralls.io/repos/github/gavv/httpexpect/badge.svg?branch=master)](https://coveralls.io/github/gavv/httpexpect?branch=master) [![GitHub release](https://img.shields.io/github/tag/gavv/httpexpect.svg)](https://github.com/gavv/httpexpect/releases)
+# httpexpect [![GoDev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/gavv/httpexpect/v2) [![Build](https://github.com/gavv/httpexpect/workflows/build/badge.svg)](https://github.com/gavv/httpexpect/actions) [![Coveralls](https://coveralls.io/repos/github/gavv/httpexpect/badge.svg?branch=master)](https://coveralls.io/github/gavv/httpexpect?branch=master) [![GitHub release](https://img.shields.io/github/tag/gavv/httpexpect.svg)](https://github.com/gavv/httpexpect/releases)
 
 Concise, declarative, and easy to use end-to-end HTTP and REST API testing for Go (golang).
 
diff --git a/array.go b/array.go
index 0b28e26..d4ea27c 100644
--- a/array.go
+++ b/array.go
@@ -211,7 +211,7 @@ func (a *Array) NotEqual(value interface{}) *Array {
 //  array.Elements("foo", 123)
 //
 // This calls are equivalent:
-//  array.Elelems("a", "b")
+//  array.Elements("a", "b")
 //  array.Equal([]interface{}{"a", "b"})
 func (a *Array) Elements(values ...interface{}) *Array {
 	return a.Equal(values)
diff --git a/request.go b/request.go
index 4b60ba3..da3b591 100644
--- a/request.go
+++ b/request.go
@@ -851,6 +851,21 @@ func (r *Request) WithMultipart() *Request {
 	return r
 }
 
+// WithHost sets request host to given string.
+//
+// Example:
+//  req := NewRequest(config, "PUT", "http://example.com/path")
+//  req.WithHost("example.com")
+func (r *Request) WithHost(host string) *Request {
+	if r.chain.failed() {
+		return r
+	}
+
+	r.http.Host = host
+
+	return r
+}
+
 // Expect constructs http.Request, sends it, receives http.Response, and
 // returns a new Response object to inspect received response.
 //
diff --git a/websocket.go b/websocket.go
index 0cfc218..a28646b 100644
--- a/websocket.go
+++ b/websocket.go
@@ -16,19 +16,29 @@ var infiniteTime = time.Time{}
 type Websocket struct {
 	config       Config
 	chain        chain
-	conn         *websocket.Conn
+	conn         WebsocketConn
 	readTimeout  time.Duration
 	writeTimeout time.Duration
 	isClosed     bool
 }
 
+// WebsocketConn is used by Websocket to communicate with actual websocket connection.
+type WebsocketConn interface {
+	ReadMessage() (messageType int, p []byte, err error)
+	WriteMessage(messageType int, data []byte) error
+	Close() error
+	SetReadDeadline(t time.Time) error
+	SetWriteDeadline(t time.Time) error
+	Subprotocol() string
+}
+
 // NewWebsocket returns a new Websocket given a Config with Reporter and
-// Printers, and websocket.Conn to be inspected and handled.
-func NewWebsocket(config Config, conn *websocket.Conn) *Websocket {
+// Printers, and Websocket to be inspected and handled.
+func NewWebsocket(config Config, conn WebsocketConn) *Websocket {
 	return makeWebsocket(config, makeChain(config.Reporter), conn)
 }
 
-func makeWebsocket(config Config, chain chain, conn *websocket.Conn) *Websocket {
+func makeWebsocket(config Config, chain chain, conn WebsocketConn) *Websocket {
 	return &Websocket{
 		config: config,
 		chain:  chain,
@@ -36,12 +46,24 @@ func makeWebsocket(config Config, chain chain, conn *websocket.Conn) *Websocket
 	}
 }
 
-// Raw returns underlying websocket.Conn object.
+// Conn returns underlying WebsocketConn object.
 // This is the value originally passed to NewConnection.
-func (c *Websocket) Raw() *websocket.Conn {
+func (c *Websocket) Conn() WebsocketConn {
 	return c.conn
 }
 
+// Deprecated: use Conn instead.
+func (c *Websocket) Raw() *websocket.Conn {
+	if c.conn == nil {
+		return nil
+	}
+	conn, ok := c.conn.(*websocket.Conn)
+	if !ok {
+		return nil
+	}
+	return conn
+}
+
 // WithReadTimeout sets timeout duration for WebSocket connection reads.
 //
 // By default no timeout is used.


Test patch:
diff --git a/e2e_websocket_test.go b/e2e_websocket_test.go
index 0ea1f73..4aaa5be 100644
--- a/e2e_websocket_test.go
+++ b/e2e_websocket_test.go
@@ -67,6 +67,22 @@ func websocketFastHandler(ctx *fasthttp.RequestCtx) {
 	}
 }
 
+func testWebsocketConn(e *Expect) {
+	ws := e.GET("/test").WithWebsocketUpgrade().
+		Expect().
+		Status(http.StatusSwitchingProtocols).
+		Websocket()
+	defer ws.Disconnect()
+
+	if ws.Conn() == nil {
+		panic("Conn returned nil")
+	}
+
+	if ws.Raw() == nil {
+		panic("Raw returned nil")
+	}
+}
+
 func testWebsocketSession(e *Expect) {
 	ws := e.GET("/test").WithWebsocketUpgrade().
 		Expect().
@@ -120,6 +136,7 @@ func testWebsocketTypes(e *Expect) {
 }
 
 func testWebsocket(e *Expect) {
+	testWebsocketConn(e)
 	testWebsocketSession(e)
 	testWebsocketTypes(e)
 }
diff --git a/mocks_test.go b/mocks_test.go
index 4398b5d..2dbacad 100644
--- a/mocks_test.go
+++ b/mocks_test.go
@@ -3,6 +3,7 @@ package httpexpect
 import (
 	"net/http"
 	"testing"
+	"time"
 )
 
 type mockClient struct {
@@ -34,3 +35,103 @@ func (r *mockReporter) Errorf(message string, args ...interface{}) {
 	r.testing.Logf("Fail: "+message, args...)
 	r.reported = true
 }
+
+type mockWebsocketConn struct {
+	msgType      int
+	readMsgErr   error
+	writeMsgErr  error
+	closeError   error
+	readDlError  error
+	writeDlError error
+	msg          []byte
+	subprotocol  string
+}
+
+func newMockWebsocketConn() *mockWebsocketConn {
+	return &mockWebsocketConn{}
+}
+
+func (wc *mockWebsocketConn) WithWriteMsgError(retError error) *mockWebsocketConn {
+	wc.writeMsgErr = retError
+	return wc
+}
+
+func (wc *mockWebsocketConn) WithReadMsgError(retError error) *mockWebsocketConn {
+	wc.readMsgErr = retError
+	return wc
+}
+
+func (wc *mockWebsocketConn) WithWriteDlError(retError error) *mockWebsocketConn {
+	wc.writeDlError = retError
+	return wc
+}
+
+func (wc *mockWebsocketConn) WithReadDlError(retError error) *mockWebsocketConn {
+	wc.readDlError = retError
+	return wc
+}
+
+func (wc *mockWebsocketConn) WithCloseError(retError error) *mockWebsocketConn {
+	wc.closeError = retError
+	return wc
+}
+
+func (wc *mockWebsocketConn) WithMsgType(msgType int) *mockWebsocketConn {
+	wc.msgType = msgType
+	return wc
+}
+
+func (wc *mockWebsocketConn) WithSubprotocol(subprotocol string) *mockWebsocketConn {
+	wc.subprotocol = subprotocol
+	return wc
+}
+
+func (wc *mockWebsocketConn) WithMessage(msg []byte) *mockWebsocketConn {
+	wc.msg = msg
+	return wc
+}
+
+func (wc *mockWebsocketConn) ReadMessage() (messageType int, p []byte, err error) {
+	return wc.msgType, []byte{}, wc.readMsgErr
+}
+func (wc *mockWebsocketConn) WriteMessage(messageType int, data []byte) error {
+	return wc.writeMsgErr
+
+}
+func (wc *mockWebsocketConn) Close() error {
+	return wc.closeError
+}
+func (wc *mockWebsocketConn) SetReadDeadline(t time.Time) error {
+	return wc.readDlError
+}
+func (wc *mockWebsocketConn) SetWriteDeadline(t time.Time) error {
+	return wc.writeDlError
+}
+func (wc *mockWebsocketConn) Subprotocol() string {
+	return wc.subprotocol
+}
+
+type MockWsPrinter struct {
+	isWrittenTo bool
+	isReadFrom  bool
+}
+
+func newMockWsPrinter() *MockWsPrinter {
+	return &MockWsPrinter{
+		isWrittenTo: false,
+		isReadFrom:  false,
+	}
+}
+
+func (pr *MockWsPrinter) Request(*http.Request) {}
+
+// Response is called after response is received.
+func (pr *MockWsPrinter) Response(*http.Response, time.Duration) {}
+
+func (pr *MockWsPrinter) WebsocketWrite(typ int, content []byte, closeCode int) {
+	pr.isWrittenTo = true
+}
+
+func (pr *MockWsPrinter) WebsocketRead(typ int, content []byte, closeCode int) {
+	pr.isReadFrom = true
+}
diff --git a/request_test.go b/request_test.go
index c089cb7..84c8db8 100644
--- a/request_test.go
+++ b/request_test.go
@@ -1453,3 +1453,66 @@ func TestRequestErrorConflictMultipart(t *testing.T) {
 	req3.WithFileBytes("a", "a", []byte("a"))
 	req3.chain.assertFailed(t)
 }
+
+func TestRequestWithHost(t *testing.T) {
+	factory1 := DefaultRequestFactory{}
+	client1 := &mockClient{}
+	reporter1 := newMockReporter(t)
+
+	config1 := Config{
+		RequestFactory: factory1,
+		Client:         client1,
+		Reporter:       reporter1,
+	}
+
+	req1 := NewRequest(config1, "METHOD", "url")
+
+	req1.WithHost("example.com")
+
+	resp := req1.Expect()
+	resp.chain.assertOK(t)
+
+	assert.Equal(t, "METHOD", client1.req.Method)
+	assert.Equal(t, "example.com", client1.req.Host)
+	assert.Equal(t, "url", client1.req.URL.String())
+
+	assert.Equal(t, &client1.resp, resp.Raw())
+
+	factory2 := DefaultRequestFactory{}
+	client2 := &mockClient{}
+	reporter2 := newMockReporter(t)
+
+	config2 := Config{
+		RequestFactory: factory2,
+		Client:         client2,
+		Reporter:       reporter2,
+	}
+
+	req2 := NewRequest(config2, "METHOD", "url")
+
+	req2.WithHeader("HOST", "example1.com")
+	req2.WithHost("example2.com")
+
+	req2.Expect().chain.assertOK(t)
+
+	assert.Equal(t, "example2.com", client2.req.Host)
+
+	factory3 := DefaultRequestFactory{}
+	client3 := &mockClient{}
+	reporter3 := newMockReporter(t)
+
+	config3 := Config{
+		RequestFactory: factory3,
+		Client:         client3,
+		Reporter:       reporter3,
+	}
+
+	req3 := NewRequest(config3, "METHOD", "url")
+
+	req3.WithHost("example2.com")
+	req3.WithHeader("HOST", "example1.com")
+
+	req3.Expect().chain.assertOK(t)
+
+	assert.Equal(t, "example1.com", client3.req.Host)
+}
diff --git a/websocket_test.go b/websocket_test.go
index 499e118..8133300 100644
--- a/websocket_test.go
+++ b/websocket_test.go
@@ -1,22 +1,25 @@
 package httpexpect
 
 import (
+	"fmt"
 	"testing"
+	"time"
 
 	"github.com/gorilla/websocket"
 )
 
+func noWsPreSteps(ws *Websocket) {}
+
 func TestWebsocketFailed(t *testing.T) {
 	chain := makeChain(newMockReporter(t))
 
 	chain.fail("fail")
 
-	ws := &Websocket{
-		chain: chain,
-	}
+	ws := makeWebsocket(Config{}, chain, nil)
 
 	ws.chain.assertFailed(t)
 
+	ws.Conn()
 	ws.Raw()
 	ws.WithReadTimeout(0)
 	ws.WithoutReadTimeout()
@@ -38,17 +41,1155 @@ func TestWebsocketFailed(t *testing.T) {
 	ws.CloseWithText("a")
 
 	ws.Disconnect()
+	ws.Close()
 }
 
-func TestWebsocketNil(t *testing.T) {
+func TestWebsocketNilConn(t *testing.T) {
 	config := Config{
 		Reporter: newMockReporter(t),
 	}
 
-	ws := NewWebsocket(config, nil)
+	t.Run("getters", func(t *testing.T) {
+		ws := NewWebsocket(config, nil)
 
-	msg := ws.Expect()
-	msg.chain.assertFailed(t)
+		if ws.Conn() != nil {
+			t.Fatal("Conn returned not nil")
+		}
 
-	ws.chain.assertFailed(t)
+		if ws.Raw() != nil {
+			t.Fatal("Raw returned not nil")
+		}
+
+		if ws.Subprotocol() == nil {
+			t.Fatal("Subprotocol returned nil")
+		}
+
+		ws.chain.assertOK(t)
+	})
+
+	t.Run("expect", func(t *testing.T) {
+		ws := NewWebsocket(config, nil)
+
+		msg := ws.Expect()
+		msg.chain.assertFailed(t)
+
+		ws.chain.assertFailed(t)
+	})
+}
+
+func TestWebsocketMockConn(t *testing.T) {
+	reporter := newMockReporter(t)
+
+	config := Config{
+		Reporter: reporter,
+	}
+
+	t.Run("getters", func(t *testing.T) {
+		ws := makeWebsocket(config, makeChain(reporter), newMockWebsocketConn())
+
+		if ws.Conn() == nil {
+			t.Fatal("Conn returned nil")
+		}
+
+		if ws.Raw() != nil {
+			t.Fatal("Raw returned not nil")
+		}
+
+		if ws.Subprotocol() == nil {
+			t.Fatal("Subprotocol returned nil")
+		}
+
+		ws.chain.assertOK(t)
+	})
+
+	t.Run("expect", func(t *testing.T) {
+		ws := makeWebsocket(config, makeChain(reporter), newMockWebsocketConn())
+
+		msg := ws.Expect()
+		msg.chain.assertOK(t)
+
+		ws.chain.assertOK(t)
+	})
+}
+
+func TestWebsocketExpect(t *testing.T) {
+	failedChain := makeChain(newMockReporter(t))
+	failedChain.fail("some previous fail...")
+
+	type args struct {
+		config     Config
+		chain      chain
+		wsConn     WebsocketConn
+		wsPreSteps func(*Websocket)
+	}
+	tests := []struct {
+		name     string
+		args     args
+		assertOk bool
+	}{
+		{
+			name: "success",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+			},
+			assertOk: true,
+		},
+		{
+			name: "fail to read message from conn",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn: newMockWebsocketConn().WithReadMsgError(
+					fmt.Errorf("failed to read message")),
+			},
+			assertOk: false,
+		},
+		{
+			name: "chain already failed",
+			args: args{
+				config:     Config{},
+				chain:      failedChain,
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+			},
+			assertOk: false,
+		},
+		{
+			name: "conn is nil",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     nil,
+			},
+			assertOk: false,
+		},
+		{
+			name: "connection closed",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsPreSteps: func(ws *Websocket) {
+					ws.Disconnect()
+				},
+				wsConn: newMockWebsocketConn(),
+			},
+			assertOk: false,
+		},
+		{
+			name: "failed to set read deadline",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn: newMockWebsocketConn().WithReadDlError(
+					fmt.Errorf("failed to set read deadline")),
+			},
+			assertOk: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ws := makeWebsocket(tt.args.config, tt.args.chain, tt.args.wsConn)
+			tt.args.wsPreSteps(ws)
+
+			ws.Expect()
+
+			if tt.assertOk {
+				ws.chain.assertOK(t)
+			} else {
+				ws.chain.assertFailed(t)
+			}
+		})
+	}
+}
+
+func TestWebsocketClose(t *testing.T) {
+
+	failedChain := makeChain(newMockReporter(t))
+	failedChain.fail("some previous fail...")
+
+	type args struct {
+		config     Config
+		chain      chain
+		wsConn     WebsocketConn
+		wsPreSteps func(*Websocket)
+		closeCode  []int
+	}
+	tests := []struct {
+		name     string
+		args     args
+		assertOk bool
+	}{
+		{
+			name: "success",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				closeCode:  []int{websocket.CloseNormalClosure},
+			},
+			assertOk: true,
+		},
+		{
+			name: "conn is nil",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:    nil,
+				closeCode: []int{websocket.CloseNormalClosure},
+			},
+			assertOk: false,
+		},
+		{
+			name: "websocket unusable",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsPreSteps: func(ws *Websocket) {
+					ws.Disconnect()
+				},
+				wsConn:    newMockWebsocketConn(),
+				closeCode: []int{websocket.CloseNormalClosure},
+			},
+			assertOk: false,
+		},
+		{
+			name: "too many close codes",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				closeCode:  []int{websocket.CloseNormalClosure, websocket.CloseAbnormalClosure},
+			},
+			assertOk: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ws := makeWebsocket(tt.args.config, tt.args.chain, tt.args.wsConn)
+
+			tt.args.wsPreSteps(ws)
+
+			ws.Close(tt.args.closeCode...)
+
+			if tt.assertOk {
+				ws.chain.assertOK(t)
+			} else {
+				ws.chain.assertFailed(t)
+			}
+		})
+	}
+}
+
+func TestWebsocketCloseWithBytes(t *testing.T) {
+
+	failedChain := makeChain(newMockReporter(t))
+	failedChain.fail("some previous fail...")
+
+	type args struct {
+		config     Config
+		chain      chain
+		wsConn     WebsocketConn
+		wsPreSteps func(*Websocket)
+		content    []byte
+		closeCode  []int
+	}
+	tests := []struct {
+		name     string
+		args     args
+		assertOk bool
+	}{
+		{
+			name: "success",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				content:    []byte("connection closed..."),
+				closeCode:  []int{websocket.CloseNormalClosure},
+			},
+			assertOk: true,
+		},
+		{
+			name: "conn is nil",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:    nil,
+				content:   []byte("connection closed..."),
+				closeCode: []int{websocket.CloseNormalClosure},
+			},
+			assertOk: false,
+		},
+		{
+			name: "websocket unusable",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsPreSteps: func(ws *Websocket) {
+					ws.Disconnect()
+				},
+				wsConn:    newMockWebsocketConn(),
+				content:   []byte("connection closed..."),
+				closeCode: []int{websocket.CloseNormalClosure},
+			},
+			assertOk: false,
+		},
+		{
+			name: "too many close codes",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				content:    []byte("connection closed..."),
+				closeCode:  []int{websocket.CloseNormalClosure, websocket.CloseAbnormalClosure},
+			},
+			assertOk: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ws := makeWebsocket(tt.args.config, tt.args.chain, tt.args.wsConn)
+
+			tt.args.wsPreSteps(ws)
+
+			ws.CloseWithBytes(tt.args.content, tt.args.closeCode...)
+
+			if tt.assertOk {
+				ws.chain.assertOK(t)
+			} else {
+				ws.chain.assertFailed(t)
+			}
+		})
+	}
+}
+
+func TestWebsocketCloseWithText(t *testing.T) {
+
+	failedChain := makeChain(newMockReporter(t))
+	failedChain.fail("some previous fail...")
+
+	type args struct {
+		config     Config
+		chain      chain
+		wsConn     WebsocketConn
+		wsPreSteps func(*Websocket)
+		content    string
+		closeCode  []int
+	}
+	tests := []struct {
+		name     string
+		args     args
+		assertOk bool
+	}{
+		{
+			name: "success",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				content:    "connection closed...",
+				closeCode:  []int{websocket.CloseNormalClosure},
+			},
+			assertOk: true,
+		},
+		{
+			name: "conn is nil",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:    nil,
+				content:   "connection closed...",
+				closeCode: []int{websocket.CloseNormalClosure},
+			},
+			assertOk: false,
+		},
+		{
+			name: "websocket unusable",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsPreSteps: func(ws *Websocket) {
+					ws.Disconnect()
+				},
+				wsConn:    newMockWebsocketConn(),
+				content:   "connection closed...",
+				closeCode: []int{websocket.CloseNormalClosure},
+			},
+			assertOk: false,
+		},
+		{
+			name: "too many close codes",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				content:    "connection closed...",
+				closeCode:  []int{websocket.CloseNormalClosure, websocket.CloseAbnormalClosure},
+			},
+			assertOk: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ws := makeWebsocket(tt.args.config, tt.args.chain, tt.args.wsConn)
+
+			tt.args.wsPreSteps(ws)
+
+			ws.CloseWithText(tt.args.content, tt.args.closeCode...)
+
+			if tt.assertOk {
+				ws.chain.assertOK(t)
+			} else {
+				ws.chain.assertFailed(t)
+			}
+		})
+	}
+}
+
+func TestWebsocketCloseWithJSON(t *testing.T) {
+
+	failedChain := makeChain(newMockReporter(t))
+	failedChain.fail("some previous fail...")
+
+	type args struct {
+		config     Config
+		chain      chain
+		wsConn     WebsocketConn
+		wsPreSteps func(*Websocket)
+		content    interface{}
+		closeCode  []int
+	}
+	tests := []struct {
+		name     string
+		args     args
+		assertOk bool
+	}{
+		{
+			name: "success",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				content: map[string]string{
+					"msg": "connection closing...",
+				},
+				closeCode: []int{websocket.CloseNormalClosure},
+			},
+			assertOk: true,
+		},
+		{
+			name: "conn is nil",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn: nil,
+				content: map[string]string{
+					"msg": "connection closing...",
+				},
+				closeCode: []int{websocket.CloseNormalClosure},
+			},
+			assertOk: false,
+		},
+		{
+			name: "websocket unusable",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsPreSteps: func(ws *Websocket) {
+					ws.Disconnect()
+				},
+				wsConn: newMockWebsocketConn(),
+				content: map[string]string{
+					"msg": "connection closing...",
+				},
+				closeCode: []int{websocket.CloseNormalClosure},
+			},
+			assertOk: false,
+		},
+		{
+			name: "too many close codes",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				content: map[string]string{
+					"msg": "connection closing...",
+				},
+				closeCode: []int{websocket.CloseNormalClosure, websocket.CloseAbnormalClosure},
+			},
+			assertOk: false,
+		},
+		{
+			name: "marshall failed",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				content:    make(chan int),
+				closeCode:  []int{websocket.CloseNormalClosure},
+			},
+			assertOk: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ws := makeWebsocket(tt.args.config, tt.args.chain, tt.args.wsConn)
+
+			tt.args.wsPreSteps(ws)
+
+			ws.CloseWithJSON(tt.args.content, tt.args.closeCode...)
+
+			if tt.assertOk {
+				ws.chain.assertOK(t)
+			} else {
+				ws.chain.assertFailed(t)
+			}
+		})
+	}
+}
+
+func TestWebsocketWriteMessage(t *testing.T) {
+	type args struct {
+		config     Config
+		chain      chain
+		wsConn     WebsocketConn
+		wsPreSteps func(*Websocket)
+		typ        int
+		content    []byte
+		closeCode  []int
+	}
+	tests := []struct {
+		name     string
+		args     args
+		assertOk bool
+	}{
+		{
+			name: "text message success",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				typ:        websocket.TextMessage,
+				content:    []byte("random message..."),
+				closeCode:  []int{},
+			},
+			assertOk: true,
+		},
+		{
+			name: "text message fail nil conn",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: nil,
+				wsPreSteps: noWsPreSteps,
+				typ:       websocket.TextMessage,
+				content:   []byte("random message..."),
+				closeCode: []int{},
+			},
+			assertOk: false,
+		},
+		{
+			name: "text message fail unusable",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: newMockWebsocketConn(),
+				wsPreSteps: func(ws *Websocket) {
+					ws.Disconnect()
+				},
+				typ:       websocket.TextMessage,
+				content:   []byte("random message..."),
+				closeCode: []int{},
+			},
+			assertOk: false,
+		},
+		{
+			name: "text message failed to set write deadline",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: newMockWebsocketConn().WithWriteDlError(
+					fmt.Errorf("failed to set write deadline")),
+				wsPreSteps: noWsPreSteps,
+				typ:        websocket.TextMessage,
+				content:    []byte("random message..."),
+				closeCode:  []int{},
+			},
+			assertOk: false,
+		},
+		{
+			name: "text message failed to write to conn",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: newMockWebsocketConn().WithWriteMsgError(
+					fmt.Errorf("failed to write message to conn")),
+				wsPreSteps: noWsPreSteps,
+				typ:        websocket.TextMessage,
+				content:    []byte("random message..."),
+				closeCode:  []int{},
+			},
+			assertOk: false,
+		},
+		{
+			name: "text binary message success",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				typ:        websocket.BinaryMessage,
+				content:    []byte("random message..."),
+				closeCode:  []int{},
+			},
+			assertOk: true,
+		},
+		{
+			name: "close message success",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsConn:     newMockWebsocketConn(),
+				wsPreSteps: noWsPreSteps,
+				typ:        websocket.CloseMessage,
+				content:    []byte("closing message..."),
+				closeCode:  []int{websocket.CloseNormalClosure},
+			},
+			assertOk: true,
+		},
+		{
+			name: "close message too many close codes",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsConn:     newMockWebsocketConn(),
+				wsPreSteps: noWsPreSteps,
+				typ:        websocket.CloseMessage,
+				content:    []byte("closing message..."),
+				closeCode:  []int{websocket.CloseNormalClosure, websocket.CloseAbnormalClosure},
+			},
+			assertOk: false,
+		},
+		{
+			name: "unsupported message type",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsConn:     newMockWebsocketConn(),
+				wsPreSteps: noWsPreSteps,
+				typ:        websocket.CloseMandatoryExtension,
+				content:    []byte("unsupported message..."),
+				closeCode:  []int{},
+			},
+			assertOk: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ws := makeWebsocket(tt.args.config, tt.args.chain, tt.args.wsConn)
+
+			tt.args.wsPreSteps(ws)
+
+			ws.WriteMessage(tt.args.typ, tt.args.content, tt.args.closeCode...)
+
+			if tt.assertOk {
+				ws.chain.assertOK(t)
+			} else {
+				ws.chain.assertFailed(t)
+			}
+		})
+	}
+}
+
+func TestWebsocketWriteBytesBinary(t *testing.T) {
+	type args struct {
+		config     Config
+		chain      chain
+		wsConn     WebsocketConn
+		wsPreSteps func(*Websocket)
+		content    []byte
+	}
+	tests := []struct {
+		name     string
+		args     args
+		assertOk bool
+	}{
+		{
+			name: "success",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				content:    []byte("random message..."),
+			},
+			assertOk: true,
+		},
+		{
+			name: "conn is nil",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: nil,
+				wsPreSteps: noWsPreSteps,
+				content: []byte("random message..."),
+			},
+			assertOk: false,
+		},
+		{
+			name: "websocket unusable",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: newMockWebsocketConn(),
+				wsPreSteps: func(ws *Websocket) {
+					ws.Disconnect()
+				},
+				content: []byte("random message..."),
+			},
+			assertOk: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ws := makeWebsocket(tt.args.config, tt.args.chain, tt.args.wsConn)
+
+			tt.args.wsPreSteps(ws)
+
+			ws.WriteBytesBinary(tt.args.content)
+
+			if tt.assertOk {
+				ws.chain.assertOK(t)
+			} else {
+				ws.chain.assertFailed(t)
+			}
+		})
+	}
+}
+
+func TestWebsocketWriteBytesText(t *testing.T) {
+	type args struct {
+		config     Config
+		chain      chain
+		wsConn     WebsocketConn
+		wsPreSteps func(*Websocket)
+		content    []byte
+	}
+	tests := []struct {
+		name     string
+		args     args
+		assertOk bool
+	}{
+		{
+			name: "success",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				content:    []byte("random message..."),
+			},
+			assertOk: true,
+		},
+		{
+			name: "conn is nil",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: nil,
+				wsPreSteps: noWsPreSteps,
+				content: []byte("random message..."),
+			},
+			assertOk: false,
+		},
+		{
+			name: "websocket unusable",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: newMockWebsocketConn(),
+				wsPreSteps: func(ws *Websocket) {
+					ws.Disconnect()
+				},
+				content: []byte("random message..."),
+			},
+			assertOk: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ws := makeWebsocket(tt.args.config, tt.args.chain, tt.args.wsConn)
+
+			tt.args.wsPreSteps(ws)
+
+			ws.WriteBytesText(tt.args.content)
+
+			if tt.assertOk {
+				ws.chain.assertOK(t)
+			} else {
+				ws.chain.assertFailed(t)
+			}
+		})
+	}
+}
+
+func TestWebsocketWriteText(t *testing.T) {
+	type args struct {
+		config     Config
+		chain      chain
+		wsConn     WebsocketConn
+		wsPreSteps func(*Websocket)
+		content    string
+	}
+	tests := []struct {
+		name     string
+		args     args
+		assertOk bool
+	}{
+		{
+			name: "success",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				content:    "random message...",
+			},
+			assertOk: true,
+		},
+		{
+			name: "conn is nil",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: nil,
+				wsPreSteps: noWsPreSteps,
+				content: "random message...",
+			},
+			assertOk: false,
+		},
+		{
+			name: "websocket unusable",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: newMockWebsocketConn(),
+				wsPreSteps: func(ws *Websocket) {
+					ws.Disconnect()
+				},
+				content: "random message...",
+			},
+			assertOk: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ws := makeWebsocket(tt.args.config, tt.args.chain, tt.args.wsConn)
+
+			tt.args.wsPreSteps(ws)
+
+			ws.WriteText(tt.args.content)
+
+			if tt.assertOk {
+				ws.chain.assertOK(t)
+			} else {
+				ws.chain.assertFailed(t)
+			}
+		})
+	}
+}
+
+func TestWebsocketWriteJSON(t *testing.T) {
+	type args struct {
+		config     Config
+		chain      chain
+		wsConn     WebsocketConn
+		wsPreSteps func(*Websocket)
+		content    interface{}
+	}
+	tests := []struct {
+		name     string
+		args     args
+		assertOk bool
+	}{
+		{
+			name: "success",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsPreSteps: noWsPreSteps,
+				wsConn:     newMockWebsocketConn(),
+				content: map[string]string{
+					"msg": "random message",
+				},
+			},
+			assertOk: true,
+		},
+		{
+			name: "conn is nil",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: nil,
+				wsPreSteps: noWsPreSteps,
+				content: map[string]string{
+					"msg": "random message",
+				},
+			},
+			assertOk: false,
+		},
+		{
+			name: "websocket unusable",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: newMockWebsocketConn(),
+				wsPreSteps: func(ws *Websocket) {
+					ws.Disconnect()
+				},
+				content: map[string]string{
+					"msg": "random message",
+				},
+			},
+			assertOk: false,
+		},
+		{
+			name: "JSON marshal failed",
+			args: args{
+				config:     Config{},
+				chain:      makeChain(newMockReporter(t)),
+				wsConn:     newMockWebsocketConn(),
+				wsPreSteps: noWsPreSteps,
+				content:    make(chan int),
+			},
+			assertOk: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ws := makeWebsocket(tt.args.config, tt.args.chain, tt.args.wsConn)
+
+			tt.args.wsPreSteps(ws)
+
+			ws.WriteJSON(tt.args.content)
+
+			if tt.assertOk {
+				ws.chain.assertOK(t)
+			} else {
+				ws.chain.assertFailed(t)
+			}
+		})
+	}
+}
+
+func TestWebsocket_Subprotocol(t *testing.T) {
+	subproto := "soap"
+	ws := NewWebsocket(Config{}, newMockWebsocketConn().WithSubprotocol(subproto))
+
+	ws.Subprotocol()
+
+	if got := ws.Subprotocol().value; got != subproto {
+		t.Errorf("Websocket.Subprotocol() = %v, want %v", got, subproto)
+	}
+}
+
+func TestWebsocket_setReadDeadline(t *testing.T) {
+	type args struct {
+		config Config
+		chain  chain
+		wsConn WebsocketConn
+	}
+	tests := []struct {
+		name     string
+		args     args
+		assertOk bool
+	}{
+		{
+			name: "success",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: newMockWebsocketConn(),
+			},
+			assertOk: true,
+		},
+		{
+			name: "conn.SetReadDeadline error",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: newMockWebsocketConn().WithReadDlError(
+					fmt.Errorf("Failed to set read deadline")),
+			},
+			assertOk: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ws := makeWebsocket(tt.args.config, tt.args.chain, tt.args.wsConn).
+				WithReadTimeout(time.Second)
+
+			ws.setReadDeadline()
+
+			if tt.assertOk {
+				ws.chain.assertOK(t)
+			} else {
+				ws.chain.assertFailed(t)
+			}
+		})
+	}
+}
+
+func TestWebsocket_setWriteDeadline(t *testing.T) {
+	type args struct {
+		config Config
+		chain  chain
+		wsConn WebsocketConn
+	}
+	tests := []struct {
+		name     string
+		args     args
+		assertOk bool
+	}{
+		{
+			name: "success",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: newMockWebsocketConn(),
+			},
+			assertOk: true,
+		},
+		{
+			name: "conn.SetReadDeadline error",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: newMockWebsocketConn().WithWriteDlError(
+					fmt.Errorf("Failed to set read deadline")),
+			},
+			assertOk: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ws := makeWebsocket(tt.args.config, tt.args.chain, tt.args.wsConn).
+				WithWriteTimeout(time.Second)
+
+			ws.setWriteDeadline()
+
+			if tt.assertOk {
+				ws.chain.assertOK(t)
+			} else {
+				ws.chain.assertFailed(t)
+			}
+		})
+	}
+}
+
+func TestWebsocket_Disconnect(t *testing.T) {
+	type args struct {
+		config Config
+		chain  chain
+		wsConn WebsocketConn
+	}
+	tests := []struct {
+		name     string
+		args     args
+		assertOk bool
+	}{
+		{
+			name: "success",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: newMockWebsocketConn(),
+			},
+			assertOk: true,
+		},
+		{
+			name: "success even if conn is nil",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: nil,
+			},
+			assertOk: true,
+		},
+		{
+			name: "conn close failed",
+			args: args{
+				config: Config{},
+				chain:  makeChain(newMockReporter(t)),
+				wsConn: newMockWebsocketConn().WithCloseError(
+					fmt.Errorf("failed to close ws conn")),
+			},
+			assertOk: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ws := makeWebsocket(tt.args.config, tt.args.chain, tt.args.wsConn)
+
+			ws.Disconnect()
+
+			if tt.assertOk {
+				ws.chain.assertOK(t)
+			} else {
+				ws.chain.assertFailed(t)
+			}
+		})
+	}
+}
+
+func TestPrintRead(t *testing.T) {
+	printer := newMockWsPrinter()
+	config := Config{
+		Printers: []Printer{printer},
+	}
+	ws := makeWebsocket(config, makeChain(newMockReporter(t)), newMockWebsocketConn())
+
+	ws.printRead(websocket.CloseMessage,
+		[]byte("random message"),
+		websocket.CloseNormalClosure)
+
+	if !printer.isReadFrom {
+		t.Errorf("Websocket.printRead() failed to read from printer")
+	}
+}
+
+func TestPrintWrite(t *testing.T) {
+	printer := newMockWsPrinter()
+	config := Config{
+		Printers: []Printer{printer},
+	}
+	ws := makeWebsocket(config, makeChain(newMockReporter(t)), newMockWebsocketConn())
+
+	ws.printWrite(websocket.CloseMessage,
+		[]byte("random message"),
+		websocket.CloseNormalClosure)
+
+	if !printer.isWrittenTo {
+		t.Errorf("Websocket.printWrite() failed to write to printer")
+	}
 }