Summary: How can I best write tests that override socket-based communication for a library, capturing what is sent over the socket, and simulating responses based on what is sent?DetailsI'm adding a new feature to ruby-mpd, and it's extensive enough that I'd like to add tests covering many scenarios.The library communicates with MPD using a socket connection, pushing and receiving streams of information, parsing that information and presenting it helpfully.Although MPD is stateful, I'm not interesting in testing the stateful nature. (That's up to the MPD team.) Instead, I want to test:when I invoke certain code, specific strings are sent over the socketwhen I invoke that codeand my test returns a reasonable response over the socketthat the results of parsing that response are what I expectI do not want to have to instantiate an MPD server and mock song library to run these tests.ExampleFor example, I need to test that this codeids = @mpd.command_list(:values) do  clear  mysongs.each{ |song| addid(song) }endcauses this to be sent over the socket:command_list_beginclearaddid fooaddid baraddid jimcommand_list_endAnd then I need to simulate that after sending that it receives this over the socketId: 1060Id: 1061Id: 1062OKand that, as a result, the return value ids is [1060,1061,1062].What I've DoneMy current plan (which may not be the best one) is to replace the real socket with a delegator that intercepts puts and gets. There's a recording socket to associate streams of commands and associate them with responses. There's a playback socket that consumes this association and returns lines of responses from the previous stream of commands.This setup does not require that sets of commands be issued in the same order, but does require that each set of commands is issued exactly the same.Current CodeRecorderclass RecordingSocket  def initialize(real_socket)    @socket = real_socket    @current = @command = []    @responses = { @command=>[] }  end  def puts(*a)    @socket.puts(*a).tap{ @command.concat(a.empty? ? [nil] : a) }  end  def gets    @socket.gets.tap do |result|      unless @command.empty?        @current,@command=@command,[]        @responses[@current] = []      end      @responses[@current] << result    end  end  def save_recording(filename=nil)    File.open(filename || socket_recording-#{Time.now.strftime '%Y%m%d_%H%M%S'}.marshal,'wb'){ |f| f << Marshal.dump(@responses) }  end  def method_missing(*a)    @socket.send(*a)  endendclass RecordingMPD < MPD  def socket    @recording_socket ||= RecordingSocket.new(super)  end  def save_recording(filename=nil)    @recording_socket.save_recording(filename)  endendRecording Responsesm = RecordingMPD.new('music.local').tap(&:connect)begin  songs = %w[test1.mp3 test2.mp3 test3.mp3]  m.command_list do    clear    s.each{ |f| addid(f) }  end  m.queue  m.playlists.find{ |pl| pl.name=='user-gkistner' }.songsensure  m.save_recording 'mpd-responses.marshal'endPlayback Setuprequire_relative '../lib/ruby-mpd'class PlaybackSocket  def initialize(filename=nil)    @responses={}    @command = []    load_recording(filename) if filename  end  def load_recording(filename)    @responses = Marshal.load(File.open(filename,'rb',&:read))    self  end  def last_messages    @current  end  def puts(*a)    @command.concat(a.empty? ? [nil] : a)  end  def gets    @current,@command=@command,[] unless @command.empty?    if @responses[@current]      @responses[@current].shift    else      raise PlaybackSocket has no (further) recording for #{@current}    end  end  def method_missing(*a)    raise PlaybackSocket has no support for #{a.shift}(#{a.map(&:inspect).join(', ')})  endendclass PlaybackMPD < MPD  attr_reader :socket  def initialize( socket_recording_file=nil )    super()    load_recording(socket_recording_file) if socket_recording_file  end  def load_recording(filename)    @socket = PlaybackSocket.new.load_recording(filename)    self  end  def last_messages    @socket.last_messages  endendRunning the Testsrequire 'minitest/autorun'class TestQueue < MiniTest::Unit::TestCase  def setup    @mpd = PlaybackMPD.new 'mpd-responses.marshal'  end  def test_songs    songs = @mpd.queue    assert_equal [playlistinfo], @mpd.last_messages    assert_equal 5, songs.length    assert songs.all?{ |value| value.is_a? MPD::Song }    assert_equal [273, 289, 129, 258, 347], songs.map(&:track_length)  end  def test_command_lists    ids = @mpd.command_list(:values) do      clear      %w[test1.mp3 test2.mp3 test3.mp3].each{ |f| addid(f) }    end    assert_equal(      [command_list_begin, clear, addid test1.mp3, addid test2.mp3,       addid test3.mp3, command_list_end],      @mpd.last_messages    )    assert_equal [101,102,103], ids  endendSpecific QuestionsIs there a better way to simulate sample data from the socket?Is there a better way to record the series-of-puts and associate them with the responses I get? I feel like there might be a bug lurking with that unless @command.empty? logic.
Testing a socket-based application
ruby;unit testing;socket