You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2025/09/25 14:40:22 UTC

(commons-io) branch master updated: Add tests

This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-io.git


The following commit(s) were added to refs/heads/master by this push:
     new 37cab2b8e Add tests
37cab2b8e is described below

commit 37cab2b8e2adf24fc5493812cb9dd41b90511098
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Thu Sep 25 10:40:17 2025 -0400

    Add tests
    
    - Remove unused exception from signature of
    CloseShieldChannelHandler.invokeObjectMethod(Object, Method, Object[])
    - Test AsynchronousChannel
    - Test AsynchronousByteChannel
    - Test NetworkChannel
    - Test MulticastChannel
    - Test InterruptibleChannel
    - Add @SuppressWarnings value
    - Inline single use local variable
    - Javadoc
    - Sort members
    - Use final
---
 src/changes/changes.xml                            |   2 +-
 .../commons/io/channels/CloseShieldChannel.java    |  57 +++---
 .../io/channels/CloseShieldChannelHandler.java     | 119 ++++++------
 .../io/channels/CloseShieldChannelTest.java        | 208 +++++++++++----------
 4 files changed, 197 insertions(+), 189 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 439c5c162..861107d82 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -66,7 +66,7 @@ The <action> type attribute can be add,update,fix,remove.
       <action dev="pkarwasz" type="add"                due-to="Piotr P. Karwasz">Add org.apache.commons.io.file.PathUtils.getPath(String, String).</action>
       <action dev="ggregory" type="add"                due-to="Gary Gregory">Add org.apache.commons.io.channels.ByteArraySeekableByteChannel.</action>
       <action dev="ggregory" type="add"                due-to="Gary Gregory">Add IOIterable.asIterable().</action>
-      <action dev="pkarwasz" type="add"                due-to="Piotr P. Karwasz">Add Channels.closeShield(Channel) for close-shielded NIO Channel proxies.</action>
+      <action dev="pkarwasz" type="add"                due-to="Piotr P. Karwasz">Add CloseShieldChannel to close-shielded NIO Channels.</action>
       <!-- UPDATE -->
       <action type="update" dev="ggregory"             due-to="Gary Gregory, Dependabot">Bump org.apache.commons:commons-parent from 85 to 88 #774, #783.</action>
       <action type="update" dev="ggregory"             due-to="Gary Gregory">[test] Bump commons-codec:commons-codec from 1.18.0 to 1.19.0.</action>
diff --git a/src/main/java/org/apache/commons/io/channels/CloseShieldChannel.java b/src/main/java/org/apache/commons/io/channels/CloseShieldChannel.java
index 00f269f9a..e100082dc 100644
--- a/src/main/java/org/apache/commons/io/channels/CloseShieldChannel.java
+++ b/src/main/java/org/apache/commons/io/channels/CloseShieldChannel.java
@@ -14,9 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.commons.io.channels;
 
-import java.lang.reflect.InvocationHandler;
+import java.io.Closeable;
 import java.lang.reflect.Proxy;
 import java.nio.channels.Channel;
 import java.util.LinkedHashSet;
@@ -24,16 +25,31 @@
 import java.util.Set;
 
 /**
- * Utility to create a close-shielding proxy for a {@link Channel}.
+ * Creates a close-shielding proxy for a {@link Channel}.
  *
- * <p>The returned proxy will implement all {@link Channel} sub-interfaces that the delegate implements.</p>
+ * <p>
+ * The returned proxy will implement all {@link Channel} sub-interfaces that the delegate implements.
+ * </p>
  *
+ * @see Channel
+ * @see Closeable
  * @since 2.21.0
  */
 public final class CloseShieldChannel {
 
-    private CloseShieldChannel() {
-        // no instance
+    private static Class<?>[] collectChannelInterfaces(final Class<?> type) {
+        final Set<Class<?>> out = new LinkedHashSet<>();
+        collectChannelInterfaces(type, out);
+        return out.toArray(new Class<?>[0]);
+    }
+
+    private static void collectChannelInterfaces(final Class<?> type, final Set<Class<?>> out) {
+        // Visit interfaces
+        for (final Class<?> iface : type.getInterfaces()) {
+            if (Channel.class.isAssignableFrom(iface) && out.add(iface)) {
+                collectChannelInterfaces(iface, out);
+            }
+        }
     }
 
     /**
@@ -43,42 +59,25 @@ private CloseShieldChannel() {
      * @param <T>     Any Channel type (interface or class).
      * @return A proxy that shields {@code close()} and enforces closed semantics on other calls.
      */
-    @SuppressWarnings("unchecked")
+    @SuppressWarnings({ "unchecked", "resource" }) // caller closes
     public static <T extends Channel> T wrap(final T channel) {
         Objects.requireNonNull(channel, "channel");
-
         // Fast path: already our shield
         if (Proxy.isProxyClass(channel.getClass())) {
-            final InvocationHandler handler = Proxy.getInvocationHandler(channel);
-            if (handler instanceof CloseShieldChannelHandler) {
+            if (Proxy.getInvocationHandler(channel) instanceof CloseShieldChannelHandler) {
                 return channel;
             }
         }
-
         // Collect only Channel sub-interfaces.
         Class<?>[] ifaces = collectChannelInterfaces(channel.getClass());
         if (ifaces.length == 0) {
-            ifaces = new Class<?>[] {Channel.class}; // fallback to minimal surface
+            ifaces = new Class<?>[] { Channel.class }; // fallback to root surface
         }
-
-        return (T) Proxy.newProxyInstance(
-                channel.getClass().getClassLoader(), // use delegate's loader
-                ifaces,
-                new CloseShieldChannelHandler(channel));
-    }
-
-    private static Class<?>[] collectChannelInterfaces(final Class<?> type) {
-        final Set<Class<?>> out = new LinkedHashSet<>();
-        collectChannelInterfaces(type, out);
-        return out.toArray(new Class<?>[0]);
+        return (T) Proxy.newProxyInstance(channel.getClass().getClassLoader(), // use delegate's loader
+                ifaces, new CloseShieldChannelHandler(channel));
     }
 
-    private static void collectChannelInterfaces(final Class<?> type, final Set<Class<?>> out) {
-        // Visit interfaces
-        for (Class<?> iface : type.getInterfaces()) {
-            if (Channel.class.isAssignableFrom(iface) && out.add(iface)) {
-                collectChannelInterfaces(iface, out);
-            }
-        }
+    private CloseShieldChannel() {
+        // no instance
     }
 }
diff --git a/src/main/java/org/apache/commons/io/channels/CloseShieldChannelHandler.java b/src/main/java/org/apache/commons/io/channels/CloseShieldChannelHandler.java
index 7fcdea4d5..67386e35c 100644
--- a/src/main/java/org/apache/commons/io/channels/CloseShieldChannelHandler.java
+++ b/src/main/java/org/apache/commons/io/channels/CloseShieldChannelHandler.java
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.commons.io.channels;
 
 import java.lang.reflect.InvocationHandler;
@@ -28,6 +29,39 @@
 
 final class CloseShieldChannelHandler implements InvocationHandler {
 
+    /**
+     * Tests whether the given method is allowed to be called after the shield is closed.
+     *
+     * @param declaringClass The class declaring the method.
+     * @param name           The method name.
+     * @param parameterCount The number of parameters.
+     * @return {@code true} if the method is allowed after {@code close()}, {@code false} otherwise.
+     */
+    private static boolean isAllowedAfterClose(final Class<?> declaringClass, final String name, final int parameterCount) {
+        // JDK explicitly allows NetworkChannel.supportedOptions() post-close
+        return parameterCount == 0 && name.equals("supportedOptions") && NetworkChannel.class.equals(declaringClass);
+    }
+
+    /**
+     * Tests whether the given method returns 'this' (the channel) as per JDK spec.
+     *
+     * @param declaringClass The class declaring the method.
+     * @param name           The method name.
+     * @param parameterCount The number of parameters.
+     * @return {@code true} if the method returns 'this', {@code false} otherwise.
+     */
+    private static boolean returnsThis(final Class<?> declaringClass, final String name, final int parameterCount) {
+        if (SeekableByteChannel.class.equals(declaringClass)) {
+            // SeekableByteChannel.position(long) and truncate(long) return 'this'
+            return parameterCount == 1 && (name.equals("position") || name.equals("truncate"));
+        }
+        if (NetworkChannel.class.equals(declaringClass)) {
+            // NetworkChannel.bind and NetworkChannel.setOption returns 'this'
+            return parameterCount == 1 && name.equals("bind") || parameterCount == 2 && name.equals("setOption");
+        }
+        return false;
+    }
+
     private final Channel delegate;
     private volatile boolean closed;
 
@@ -40,96 +74,57 @@ public Object invoke(final Object proxy, final Method method, final Object[] arg
         final Class<?> declaringClass = method.getDeclaringClass();
         final String name = method.getName();
         final int parameterCount = method.getParameterCount();
-
         // 1) java.lang.Object methods
         if (declaringClass == Object.class) {
             return invokeObjectMethod(proxy, method, args);
         }
-
         // 2) Channel.close(): mark shield closed, do NOT close the delegate
         if (parameterCount == 0 && name.equals("close")) {
             closed = true;
             return null;
         }
-
         // 3) Channel.isOpen(): reflect shield state only
         if (parameterCount == 0 && name.equals("isOpen")) {
             return !closed && delegate.isOpen();
         }
-
         // 4) After the shield is closed, only allow a tiny allowlist of safe queries
         if (closed && !isAllowedAfterClose(declaringClass, name, parameterCount)) {
             throw new ClosedChannelException();
         }
-
         // 5) Delegate to the underlying channel and unwrap target exceptions
         try {
             final Object result = method.invoke(delegate, args);
             return returnsThis(declaringClass, name, parameterCount) ? proxy : result;
-        } catch (InvocationTargetException e) {
+        } catch (final InvocationTargetException e) {
             throw e.getCause();
         }
     }
 
-    /**
-     * Tests whether the given method is allowed to be called after the shield is closed.
-     *
-     * @param declaringClass The class declaring the method.
-     * @param name           The method name.
-     * @param parameterCount The number of parameters.
-     * @return {@code true} if the method is allowed after {@code close()}, {@code false} otherwise.
-     */
-    private static boolean isAllowedAfterClose(Class<?> declaringClass, String name, int parameterCount) {
-        // JDK explicitly allows NetworkChannel.supportedOptions() post-close
-        return parameterCount == 0 && name.equals("supportedOptions") && NetworkChannel.class.equals(declaringClass);
-    }
-
-    /**
-     * Tests whether the given method returns 'this' (the channel) as per JDK spec.
-     *
-     * @param declaringClass The class declaring the method.
-     * @param name           The method name.
-     * @param parameterCount The number of parameters.
-     * @return {@code true} if the method returns 'this', {@code false} otherwise.
-     */
-    private static boolean returnsThis(Class<?> declaringClass, String name, int parameterCount) {
-        if (SeekableByteChannel.class.equals(declaringClass)) {
-            // SeekableByteChannel.position(long) and truncate(long) return 'this'
-            return parameterCount == 1 && (name.equals("position") || name.equals("truncate"));
-        }
-        if (NetworkChannel.class.equals(declaringClass)) {
-            // NetworkChannel.bind and NetworkChannel.setOption returns 'this'
-            return parameterCount == 1 && name.equals("bind") || parameterCount == 2 && name.equals("setOption");
-        }
-        return false;
-    }
-
-    private Object invokeObjectMethod(final Object proxy, final Method method, final Object[] args)
-            throws ReflectiveOperationException {
+    private Object invokeObjectMethod(final Object proxy, final Method method, final Object[] args) {
         switch (method.getName()) {
-            case "toString":
-                return "CloseShield(" + delegate + ")";
-            case "hashCode":
-                return Objects.hashCode(delegate);
-            case "equals": {
-                final Object other = args[0];
-                if (other == null) {
-                    return false;
-                }
-                if (proxy == other) {
-                    return true;
-                }
-                if (Proxy.isProxyClass(other.getClass())) {
-                    final InvocationHandler h = Proxy.getInvocationHandler(other);
-                    if (h instanceof CloseShieldChannelHandler) {
-                        return Objects.equals(((CloseShieldChannelHandler) h).delegate, this.delegate);
-                    }
-                }
+        case "toString":
+            return "CloseShield(" + delegate + ")";
+        case "hashCode":
+            return Objects.hashCode(delegate);
+        case "equals": {
+            final Object other = args[0];
+            if (other == null) {
                 return false;
             }
-            default:
-                // Not possible, all non-final Object methods are handled above
-                return null;
+            if (proxy == other) {
+                return true;
+            }
+            if (Proxy.isProxyClass(other.getClass())) {
+                final InvocationHandler h = Proxy.getInvocationHandler(other);
+                if (h instanceof CloseShieldChannelHandler) {
+                    return Objects.equals(((CloseShieldChannelHandler) h).delegate, this.delegate);
+                }
+            }
+            return false;
+        }
+        default:
+            // Not possible, all non-final Object methods are handled above
+            return null;
         }
     }
 }
diff --git a/src/test/java/org/apache/commons/io/channels/CloseShieldChannelTest.java b/src/test/java/org/apache/commons/io/channels/CloseShieldChannelTest.java
index 6b704ad1e..ac7e85b7a 100644
--- a/src/test/java/org/apache/commons/io/channels/CloseShieldChannelTest.java
+++ b/src/test/java/org/apache/commons/io/channels/CloseShieldChannelTest.java
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.commons.io.channels;
 
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@@ -31,10 +32,14 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
+import java.nio.channels.AsynchronousByteChannel;
+import java.nio.channels.AsynchronousChannel;
 import java.nio.channels.ByteChannel;
 import java.nio.channels.Channel;
 import java.nio.channels.ClosedChannelException;
 import java.nio.channels.GatheringByteChannel;
+import java.nio.channels.InterruptibleChannel;
+import java.nio.channels.MulticastChannel;
 import java.nio.channels.NetworkChannel;
 import java.nio.channels.ReadableByteChannel;
 import java.nio.channels.ScatteringByteChannel;
@@ -46,32 +51,54 @@
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 
+/**
+ * Tests {@link CloseShieldChannel}.
+ */
 class CloseShieldChannelTest {
 
     static Stream<Class<? extends Channel>> testedInterfaces() {
+        // @formatter:off
         return Stream.of(
+                AsynchronousByteChannel.class,
+                AsynchronousChannel.class,
+                ByteChannel.class,
                 Channel.class,
+                GatheringByteChannel.class,
+                InterruptibleChannel.class,
+                MulticastChannel.class,
+                NetworkChannel.class,
+                NetworkChannel.class,
                 ReadableByteChannel.class,
                 ScatteringByteChannel.class,
-                WritableByteChannel.class,
-                GatheringByteChannel.class,
-                ByteChannel.class,
                 SeekableByteChannel.class,
-                NetworkChannel.class);
+                WritableByteChannel.class);
+        // @formatter:on
     }
 
     @ParameterizedTest
     @MethodSource("testedInterfaces")
-    void testPreservesInterfaces(Class<? extends Channel> channelClass) {
+    void testCloseDoesNotCloseDelegate(final Class<? extends Channel> channelClass) throws Exception {
         final Channel channel = mock(channelClass);
         final Channel shield = CloseShieldChannel.wrap(channel);
-        assertNotSame(channel, shield);
-        assertTrue(channelClass.isInstance(shield));
+        shield.close();
+        verify(channel, never()).close();
     }
 
     @ParameterizedTest
     @MethodSource("testedInterfaces")
-    void testCloseIsShielded(Class<? extends Channel> channelClass) throws Exception {
+    void testCloseIsIdempotent(final Class<? extends Channel> channelClass) throws Exception {
+        final Channel channel = mock(channelClass);
+        final Channel shield = CloseShieldChannel.wrap(channel);
+        shield.close();
+        assertFalse(shield.isOpen());
+        shield.close();
+        assertFalse(shield.isOpen());
+        verifyNoInteractions(channel);
+    }
+
+    @ParameterizedTest
+    @MethodSource("testedInterfaces")
+    void testCloseIsShielded(final Class<? extends Channel> channelClass) throws Exception {
         final Channel channel = mock(channelClass);
         when(channel.isOpen()).thenReturn(true, false, true, false);
         final Channel shield = CloseShieldChannel.wrap(channel);
@@ -79,7 +106,6 @@ void testCloseIsShielded(Class<? extends Channel> channelClass) throws Exception
         assertTrue(shield.isOpen(), "isOpen reflects delegate state");
         assertFalse(shield.isOpen(), "isOpen reflects delegate state");
         verify(channel, times(2)).isOpen();
-
         shield.close();
         // Reflects shield state after close
         assertFalse(shield.isOpen(), "isOpen reflects shield state");
@@ -87,30 +113,17 @@ void testCloseIsShielded(Class<? extends Channel> channelClass) throws Exception
         verify(channel, times(2)).isOpen();
     }
 
-    @ParameterizedTest
-    @MethodSource("testedInterfaces")
-    void testCloseDoesNotCloseDelegate(Class<? extends Channel> channelClass) throws Exception {
-        final Channel channel = mock(channelClass);
-        final Channel shield = CloseShieldChannel.wrap(channel);
-        shield.close();
-        verify(channel, never()).close();
-    }
-
-    @ParameterizedTest
-    @MethodSource("testedInterfaces")
-    void testCloseIsIdempotent(Class<? extends Channel> channelClass) throws Exception {
-        final Channel channel = mock(channelClass);
-        final Channel shield = CloseShieldChannel.wrap(channel);
-        shield.close();
-        assertFalse(shield.isOpen());
-        shield.close();
-        assertFalse(shield.isOpen());
-        verifyNoInteractions(channel);
+    @Test
+    void testDoesNotDoubleWrap() {
+        final ByteChannel channel = mock(ByteChannel.class);
+        final ByteChannel shield1 = CloseShieldChannel.wrap(channel);
+        final ByteChannel shield2 = CloseShieldChannel.wrap(shield1);
+        assertSame(shield1, shield2);
     }
 
     @ParameterizedTest
     @MethodSource("testedInterfaces")
-    void testEquals(Class<? extends Channel> channelClass) throws Exception {
+    void testEquals(final Class<? extends Channel> channelClass) throws Exception {
         final Channel channel = mock(channelClass);
         final Channel shield = CloseShieldChannel.wrap(channel);
         final Channel anotherShield = CloseShieldChannel.wrap(channel);
@@ -120,9 +133,24 @@ void testEquals(Class<? extends Channel> channelClass) throws Exception {
         assertTrue(shield.equals(anotherShield), "shields of same delegate are equal");
     }
 
+    @Test
+    void testGatheringByteChannelMethods() throws Exception {
+        final GatheringByteChannel channel = mock(GatheringByteChannel.class);
+        when(channel.isOpen()).thenReturn(true);
+        final GatheringByteChannel shield = CloseShieldChannel.wrap(channel);
+        // Before close write() should delegate
+        when(channel.write(null, 0, 0)).thenReturn(42L);
+        assertEquals(42, shield.write(null, 0, 0));
+        verify(channel).write(null, 0, 0);
+        // After close write() should throw ClosedChannelException
+        shield.close();
+        assertThrows(ClosedChannelException.class, () -> shield.write(null, 0, 0));
+        verifyNoMoreInteractions(channel);
+    }
+
     @ParameterizedTest
     @MethodSource("testedInterfaces")
-    void testHashCode(Class<? extends Channel> channelClass) throws Exception {
+    void testHashCode(final Class<? extends Channel> channelClass) throws Exception {
         final Channel channel = mock(channelClass);
         final Channel shield = CloseShieldChannel.wrap(channel);
         final Channel anotherShield = CloseShieldChannel.wrap(channel);
@@ -130,15 +158,43 @@ void testHashCode(Class<? extends Channel> channelClass) throws Exception {
         assertEquals(shield.hashCode(), anotherShield.hashCode(), "shields of same delegate have same hashCode");
     }
 
+    @Test
+    void testNetworkChannelMethods() throws Exception {
+        final NetworkChannel channel = mock(NetworkChannel.class);
+        when(channel.isOpen()).thenReturn(true);
+        final NetworkChannel shield = CloseShieldChannel.wrap(channel);
+        // Before close getOption(), setOption(), getLocalAddress() and bind() should delegate
+        when(channel.getOption(null)).thenReturn("foo");
+        when(channel.setOption(null, null)).thenReturn(channel);
+        when(channel.getLocalAddress()).thenReturn(null);
+        when(channel.bind(null)).thenReturn(channel);
+        assertEquals("foo", shield.getOption(null));
+        assertEquals(shield, shield.setOption(null, null));
+        assertEquals(null, shield.getLocalAddress());
+        assertEquals(shield, shield.bind(null));
+        verify(channel).getOption(null);
+        verify(channel).setOption(null, null);
+        verify(channel).getLocalAddress();
+        verify(channel).bind(null);
+        // After close supportedOptions() should still work
+        shield.close();
+        assertDoesNotThrow(shield::supportedOptions);
+        verify(channel).supportedOptions();
+        // But the remaining methods should throw ClosedChannelException
+        assertThrows(ClosedChannelException.class, () -> shield.setOption(null, null));
+        assertThrows(ClosedChannelException.class, () -> shield.getOption(null));
+        assertThrows(ClosedChannelException.class, shield::getLocalAddress);
+        assertThrows(ClosedChannelException.class, () -> shield.bind(null));
+        verifyNoMoreInteractions(channel);
+    }
+
     @ParameterizedTest
     @MethodSource("testedInterfaces")
-    void testToString(Class<? extends Channel> channelClass) throws Exception {
+    void testPreservesInterfaces(final Class<? extends Channel> channelClass) {
         final Channel channel = mock(channelClass);
-        when(channel.toString()).thenReturn("MyChannel");
         final Channel shield = CloseShieldChannel.wrap(channel);
-        final String shieldString = shield.toString();
-        assertTrue(shieldString.contains("CloseShield"));
-        assertTrue(shieldString.contains("MyChannel"));
+        assertNotSame(channel, shield);
+        assertTrue(channelClass.isInstance(shield));
     }
 
     @Test
@@ -171,36 +227,6 @@ void testScatteringByteChannelMethods() throws Exception {
         verifyNoMoreInteractions(channel);
     }
 
-    @Test
-    void testWritableByteChannelMethods() throws Exception {
-        final WritableByteChannel channel = mock(WritableByteChannel.class);
-        when(channel.isOpen()).thenReturn(true);
-        final WritableByteChannel shield = CloseShieldChannel.wrap(channel);
-        // Before close write() should delegate
-        when(channel.write(null)).thenReturn(42);
-        assertEquals(42, shield.write(null));
-        verify(channel).write(null);
-        // After close write() should throw ClosedChannelException
-        shield.close();
-        assertThrows(ClosedChannelException.class, () -> shield.write(null));
-        verifyNoMoreInteractions(channel);
-    }
-
-    @Test
-    void testGatheringByteChannelMethods() throws Exception {
-        final GatheringByteChannel channel = mock(GatheringByteChannel.class);
-        when(channel.isOpen()).thenReturn(true);
-        final GatheringByteChannel shield = CloseShieldChannel.wrap(channel);
-        // Before close write() should delegate
-        when(channel.write(null, 0, 0)).thenReturn(42L);
-        assertEquals(42, shield.write(null, 0, 0));
-        verify(channel).write(null, 0, 0);
-        // After close write() should throw ClosedChannelException
-        shield.close();
-        assertThrows(ClosedChannelException.class, () -> shield.write(null, 0, 0));
-        verifyNoMoreInteractions(channel);
-    }
-
     @Test
     void testSeekableByteChannelMethods() throws Exception {
         final SeekableByteChannel channel = mock(SeekableByteChannel.class);
@@ -229,41 +255,29 @@ void testSeekableByteChannelMethods() throws Exception {
         verifyNoMoreInteractions(channel);
     }
 
+    @ParameterizedTest
+    @MethodSource("testedInterfaces")
+    void testToString(final Class<? extends Channel> channelClass) throws Exception {
+        final Channel channel = mock(channelClass);
+        when(channel.toString()).thenReturn("MyChannel");
+        final Channel shield = CloseShieldChannel.wrap(channel);
+        final String shieldString = shield.toString();
+        assertTrue(shieldString.contains("CloseShield"));
+        assertTrue(shieldString.contains("MyChannel"));
+    }
+
     @Test
-    void testNetworkChannelMethods() throws Exception {
-        final NetworkChannel channel = mock(NetworkChannel.class);
+    void testWritableByteChannelMethods() throws Exception {
+        final WritableByteChannel channel = mock(WritableByteChannel.class);
         when(channel.isOpen()).thenReturn(true);
-        final NetworkChannel shield = CloseShieldChannel.wrap(channel);
-        // Before close getOption(), setOption(), getLocalAddress() and bind() should delegate
-        when(channel.getOption(null)).thenReturn("foo");
-        when(channel.setOption(null, null)).thenReturn(channel);
-        when(channel.getLocalAddress()).thenReturn(null);
-        when(channel.bind(null)).thenReturn(channel);
-        assertEquals("foo", shield.getOption(null));
-        assertEquals(shield, shield.setOption(null, null));
-        assertEquals(null, shield.getLocalAddress());
-        assertEquals(shield, shield.bind(null));
-        verify(channel).getOption(null);
-        verify(channel).setOption(null, null);
-        verify(channel).getLocalAddress();
-        verify(channel).bind(null);
-        // After close supportedOptions() should still work
+        final WritableByteChannel shield = CloseShieldChannel.wrap(channel);
+        // Before close write() should delegate
+        when(channel.write(null)).thenReturn(42);
+        assertEquals(42, shield.write(null));
+        verify(channel).write(null);
+        // After close write() should throw ClosedChannelException
         shield.close();
-        assertDoesNotThrow(shield::supportedOptions);
-        verify(channel).supportedOptions();
-        // But the remaining methods should throw ClosedChannelException
-        assertThrows(ClosedChannelException.class, () -> shield.setOption(null, null));
-        assertThrows(ClosedChannelException.class, () -> shield.getOption(null));
-        assertThrows(ClosedChannelException.class, shield::getLocalAddress);
-        assertThrows(ClosedChannelException.class, () -> shield.bind(null));
+        assertThrows(ClosedChannelException.class, () -> shield.write(null));
         verifyNoMoreInteractions(channel);
     }
-
-    @Test
-    void testDoesNotDoubleWrap() {
-        final ByteChannel channel = mock(ByteChannel.class);
-        final ByteChannel shield1 = CloseShieldChannel.wrap(channel);
-        final ByteChannel shield2 = CloseShieldChannel.wrap(shield1);
-        assertSame(shield1, shield2);
-    }
 }