diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..744a412
--- /dev/null
+++ b/.rspec
@@ -0,0 +1,5 @@
+--color
+--format documentation
+--backtrace
+--require spec_helper
+--warnings
diff --git a/.travis.yml b/.travis.yml
index e1ea1fe..e8052cf 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,20 @@
+language: ruby
+cache: bundler
+
 rvm:
-  - 1.9.3
-  - 2.0.0
-  - jruby
-branches:
-  only:
-    - master
+  - 2.0
+  - 2.1
+  - 2.2
+  - 2.3
+  - 2.4
+  - 2.5
+  - 2.6
+  - ruby-head
+  - jruby-head
+  - truffleruby
+
+matrix:
+  allow_failures:
+    - rvm: ruby-head
+    - rvm: jruby-head
+    - rvm: truffleruby
diff --git a/.yardopts b/.yardopts
new file mode 100644
index 0000000..12595ba
--- /dev/null
+++ b/.yardopts
@@ -0,0 +1,6 @@
+--no-private
+lib/**/*.rb
+-
+History.txt
+LICENSE
+README.md
diff --git a/Gemfile b/Gemfile
index e55151b..0c1a000 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,14 +1,6 @@
 source 'https://rubygems.org'
 gemspec
 
-platforms :mri_19 do
-  gem 'ruby-debug19'
-end
-
-platforms :mri_18 do
-  gem 'ruby-debug'
-end
-
 group :development, :test do
   gem 'rake'
 end
diff --git a/History.txt b/History.txt
index 64b1e38..85ef2a3 100644
--- a/History.txt
+++ b/History.txt
@@ -1,3 +1,8 @@
+<!--
+# @markup rdoc
+# @title CHANGELOG
+-->
+
 === 2.0.0 / 2013-12-21
 
 - Drop Ruby 1.8 compatibility
@@ -57,4 +62,3 @@
 * 1 major enhancement
 
   * Birthday!
-
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ce550a3
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2007-2013 Nick Sieger nick@nicksieger.com
+Copyright, 2017, by Samuel G. D. Williams.
+
+MIT license.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
index fc7eae1..427a804 100644
--- a/README.md
+++ b/README.md
@@ -1,77 +1,127 @@
-## multipart-post
+# Multipart::Post
 
-* http://github.com/nicksieger/multipart-post
+Adds a streamy multipart form post capability to `Net::HTTP`. Also supports other
+methods besides `POST`.
 
-![build status](https://travis-ci.org/nicksieger/multipart-post.png)
+[![Build Status](https://secure.travis-ci.org/socketry/multipart-post.svg)](http://travis-ci.org/socketry/multipart-post)
 
-#### DESCRIPTION:
-
-Adds a streamy multipart form post capability to Net::HTTP. Also
-supports other methods besides POST.
-
-#### FEATURES/PROBLEMS:
+## Features/Problems
 
 * Appears to actually work. A good feature to have.
-* Encapsulates posting of file/binary parts and name/value parameter parts, similar to 
+* Encapsulates posting of file/binary parts and name/value parameter parts, similar to
   most browsers' file upload forms.
-* Provides an UploadIO helper class to prepare IO objects for inclusion in the params
+* Provides an `UploadIO` helper class to prepare IO objects for inclusion in the params
   hash of the multipart post object.
 
-#### SYNOPSIS:
-
-    require 'net/http/post/multipart'
-
-    url = URI.parse('http://www.example.com/upload')
-    File.open("./image.jpg") do |jpg|
-      req = Net::HTTP::Post::Multipart.new url.path,
-        "file" => UploadIO.new(jpg, "image/jpeg", "image.jpg")
-      res = Net::HTTP.start(url.host, url.port) do |http|
-        http.request(req)
-      end
-    end
-
-To post multiple files or attachments, simply include multiple parameters with
-UploadIO values:
-
-    require 'net/http/post/multipart'
-
-    url = URI.parse('http://www.example.com/upload')
-    req = Net::HTTP::Post::Multipart.new url.path,
-      "file1" => UploadIO.new(File.new("./image.jpg"), "image/jpeg", "image.jpg"),
-      "file2" => UploadIO.new(File.new("./image2.jpg"), "image/jpeg", "image2.jpg")
-    res = Net::HTTP.start(url.host, url.port) do |http|
-      http.request(req)
-    end
-
-#### REQUIREMENTS:
-
-None
-
-#### INSTALL:
+## Installation
 
     gem install multipart-post
 
-#### LICENSE:
+or in your Gemfile
 
-(The MIT License)
+    gem 'multipart-post'
 
-Copyright (c) 2007-2013 Nick Sieger <nick@nicksieger.com>
+## Usage
 
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-'Software'), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
+```ruby
+require 'net/http/post/multipart'
 
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
+url = URI.parse('http://www.example.com/upload')
+File.open("./image.jpg") do |jpg|
+  req = Net::HTTP::Post::Multipart.new url.path,
+    "file" => UploadIO.new(jpg, "image/jpeg", "image.jpg")
+  res = Net::HTTP.start(url.host, url.port) do |http|
+    http.request(req)
+  end
+end
+```
 
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+To post multiple files or attachments, simply include multiple parameters with
+`UploadIO` values:
+
+```ruby
+require 'net/http/post/multipart'
+
+url = URI.parse('http://www.example.com/upload')
+req = Net::HTTP::Post::Multipart.new url.path,
+  "file1" => UploadIO.new(File.new("./image.jpg"), "image/jpeg", "image.jpg"),
+  "file2" => UploadIO.new(File.new("./image2.jpg"), "image/jpeg", "image2.jpg")
+res = Net::HTTP.start(url.host, url.port) do |http|
+  http.request(req)
+end
+```
+
+To post files with other normal, non-file params such as input values, you need to pass hashes to the `Multipart.new` method.
+
+In Rails 4 for example:
+
+```ruby
+def model_params
+  require_params = params.require(:model).permit(:param_one, :param_two, :param_three, :avatar)
+  require_params[:avatar] = model_params[:avatar].present? ? UploadIO.new(model_params[:avatar].tempfile, model_params[:avatar].content_type, model_params[:avatar].original_filename) : nil
+  require_params
+end
+
+require 'net/http/post/multipart'
+
+url = URI.parse('http://www.example.com/upload')
+Net::HTTP.start(url.host, url.port) do |http|
+  req = Net::HTTP::Post::Multipart.new(url, model_params)
+  key = "authorization_key"
+  req.add_field("Authorization", key) #add to Headers
+  http.use_ssl = (url.scheme == "https")
+  http.request(req)
+end
+```
+
+Or in plain ruby:
+
+```ruby
+def params(file)
+  params = { "description" => "A nice picture!" }
+  params[:datei] = UploadIO.new(file, "image/jpeg", "image.jpg")
+  params
+end
+
+url = URI.parse('http://www.example.com/upload')
+File.open("./image.jpg") do |file|
+  req = Net::HTTP::Post::Multipart.new(url.path, params(file))
+  res = Net::HTTP.start(url.host, url.port) do |http|
+    return http.request(req).body
+  end
+end
+```
+
+### Debugging
+
+You can debug requests and responses (e.g. status codes) for all requests by adding the following code:
+
+```ruby
+http = Net::HTTP.new(uri.host, uri.port)
+http.set_debug_output($stdout)
+```
+
+## License
+
+Released under the MIT license.
+
+Copyright (c) 2007-2013 Nick Sieger <nick@nicksieger.com>  
+Copyright, 2017, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/Rakefile b/Rakefile
index dd85431..f5cbbfd 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,9 +1,6 @@
 require "bundler/gem_tasks"
+require "rspec/core/rake_task"
 
-task :default => :test
+RSpec::Core::RakeTask.new(:test)
 
-require 'rake/testtask'
-Rake::TestTask.new do |t|
-  t.libs << "test"
-  t.test_files = FileList['test/**/test*.rb']
-end
+task :default => :test
diff --git a/debian/changelog b/debian/changelog
index 4720a28..2dc3fa5 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+ruby-multipart-post (2.1.1-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sun, 15 May 2022 14:39:28 -0000
+
 ruby-multipart-post (2.0.0-2) unstable; urgency=medium
 
   [ Utkarsh Gupta ]
diff --git a/lib/composite_io.rb b/lib/composite_io.rb
index 4ba7cf5..7fcdc70 100644
--- a/lib/composite_io.rb
+++ b/lib/composite_io.rb
@@ -7,11 +7,11 @@
 # Concatenate together multiple IO objects into a single, composite IO object
 # for purposes of reading as a single stream.
 #
-# Usage:
-#
-#     crio = CompositeReadIO.new(StringIO.new('one'), StringIO.new('two'), StringIO.new('three'))
+# @example
+#     crio = CompositeReadIO.new(StringIO.new('one'),
+#                                StringIO.new('two'),
+#                                StringIO.new('three'))
 #     puts crio.read # => "onetwothree"
-#
 class CompositeReadIO
   # Create a new composite-read IO from the arguments, all of which should
   # respond to #read in a manner consistent with IO.
@@ -56,6 +56,8 @@ end
 
 # Convenience methods for dealing with files and IO that are to be uploaded.
 class UploadIO
+  attr_reader :content_type, :original_filename, :local_path, :io, :opts
+
   # Create an upload IO suitable for including in the params hash of a
   # Net::HTTP::Post::Multipart.
   #
@@ -67,13 +69,9 @@ class UploadIO
   # uploading directly from a form in a framework, which often save the file to
   # an arbitrarily named RackMultipart file in /tmp).
   #
-  # Usage:
-  #
+  # @example
   #     UploadIO.new("file.txt", "text/plain")
   #     UploadIO.new(file_io, "text/plain", "file.txt")
-  #
-  attr_reader :content_type, :original_filename, :local_path, :io, :opts
-
   def initialize(filename_or_io, content_type, filename = nil, opts = {})
     io = filename_or_io
     local_path = ""
@@ -95,7 +93,9 @@ class UploadIO
   end
 
   def self.convert!(io, content_type, original_filename, local_path)
-    raise ArgumentError, "convert! has been removed. You must now wrap IOs using:\nUploadIO.new(filename_or_io, content_type, filename=nil)\nPlease update your code."
+    raise ArgumentError, "convert! has been removed. You must now wrap IOs " \
+                         "using:\nUploadIO.new(filename_or_io, content_type, " \
+                         "filename=nil)\nPlease update your code."
   end
 
   def method_missing(*args)
diff --git a/lib/multipart_post.rb b/lib/multipart_post.rb
index 76540a8..3a91cde 100644
--- a/lib/multipart_post.rb
+++ b/lib/multipart_post.rb
@@ -5,5 +5,5 @@
 #++
 
 module MultipartPost
-  VERSION = "2.0.0"
+  VERSION = "2.1.1"
 end
diff --git a/lib/multipartable.rb b/lib/multipartable.rb
index 28fa41e..6a96575 100644
--- a/lib/multipartable.rb
+++ b/lib/multipartable.rb
@@ -5,25 +5,44 @@
 #++
 
 require 'parts'
-  module Multipartable
-    DEFAULT_BOUNDARY = "-----------RubyMultipartPost"
-    def initialize(path, params, headers={}, boundary = DEFAULT_BOUNDARY)
-      headers = headers.clone # don't want to modify the original variable
-      parts_headers = headers.delete(:parts) || {}
-      super(path, headers)
-      parts = params.map do |k,v|
-        case v
-        when Array
-          v.map {|item| Parts::Part.new(boundary, k, item, parts_headers[k]) }
-        else
-          Parts::Part.new(boundary, k, v, parts_headers[k])
-        end
-      end.flatten
-      parts << Parts::EpiloguePart.new(boundary)
-      ios = parts.map {|p| p.to_io }
-      self.set_content_type(headers["Content-Type"] || "multipart/form-data",
-                            { "boundary" => boundary })
-      self.content_length = parts.inject(0) {|sum,i| sum + i.length }
-      self.body_stream = CompositeReadIO.new(*ios)
-    end
+require 'securerandom'
+
+module Multipartable
+  def self.secure_boundary
+    # https://tools.ietf.org/html/rfc7230
+    #      tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
+    #                     / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
+    #                     / DIGIT / ALPHA
+    
+    # https://tools.ietf.org/html/rfc2046
+    #      bcharsnospace := DIGIT / ALPHA / "'" / "(" / ")" /
+    #                       "+" / "_" / "," / "-" / "." /
+    #                       "/" / ":" / "=" / "?"
+    
+    "--#{SecureRandom.uuid}"
+  end
+  
+  def initialize(path, params, headers={}, boundary = Multipartable.secure_boundary)
+    headers = headers.clone # don't want to modify the original variable
+    parts_headers = headers.delete(:parts) || {}
+    super(path, headers)
+    parts = params.map do |k,v|
+      case v
+      when Array
+        v.map {|item| Parts::Part.new(boundary, k, item, parts_headers[k]) }
+      else
+        Parts::Part.new(boundary, k, v, parts_headers[k])
+      end
+    end.flatten
+    parts << Parts::EpiloguePart.new(boundary)
+    ios = parts.map {|p| p.to_io }
+    self.set_content_type(headers["Content-Type"] || "multipart/form-data",
+                          { "boundary" => boundary })
+    self.content_length = parts.inject(0) {|sum,i| sum + i.length }
+    self.body_stream = CompositeReadIO.new(*ios)
+    
+    @boundary = boundary
   end
+  
+  attr :boundary
+end
diff --git a/lib/net/http/post/multipart.rb b/lib/net/http/post/multipart.rb
index 7570582..dc53599 100644
--- a/lib/net/http/post/multipart.rb
+++ b/lib/net/http/post/multipart.rb
@@ -11,14 +11,15 @@ require 'composite_io'
 require 'multipartable'
 require 'parts'
 
-module Net #:nodoc:
-  class HTTP #:nodoc:
+module Net
+  class HTTP
     class Put
       class Multipart < Put
         include Multipartable
       end
     end
-    class Post #:nodoc:
+
+    class Post
       class Multipart < Post
         include Multipartable
       end
diff --git a/lib/parts.rb b/lib/parts.rb
index c06cbd9..ee46c72 100644
--- a/lib/parts.rb
+++ b/lib/parts.rb
@@ -5,7 +5,7 @@
 #++
 
 module Parts
-  module Part #:nodoc:
+  module Part
     def self.new(boundary, name, value, headers = {})
       headers ||= {} # avoid nil values
       if file?(value)
@@ -28,8 +28,14 @@ module Parts
     end
   end
 
+  # Represents a parametric part to be filled with given value.
   class ParamPart
     include Part
+
+    # @param boundary [String]
+    # @param name [#to_s]
+    # @param value [String]
+    # @param headers [Hash] Content-Type is used, if present.
     def initialize(boundary, name, value, headers = {})
       @part = build_part(boundary, name, value, headers)
       @io = StringIO.new(@part)
@@ -39,6 +45,10 @@ module Parts
      @part.bytesize
     end
 
+    # @param boundary [String]
+    # @param name [#to_s]
+    # @param value [String]
+    # @param headers [Hash] Content-Type is used, if present.
     def build_part(boundary, name, value, headers = {})
       part = ''
       part << "--#{boundary}\r\n"
@@ -52,7 +62,13 @@ module Parts
   # Represents a part to be filled from file IO.
   class FilePart
     include Part
+
     attr_reader :length
+
+    # @param boundary [String]
+    # @param name [#to_s]
+    # @param io [IO]
+    # @param headers [Hash]
     def initialize(boundary, name, io, headers = {})
       file_length = io.respond_to?(:length) ?  io.length : File.size(io.local_path)
       @head = build_head(boundary, name, io.original_filename, io.content_type, file_length,
@@ -62,25 +78,38 @@ module Parts
       @io = CompositeReadIO.new(StringIO.new(@head), io, StringIO.new(@foot))
     end
 
-    def build_head(boundary, name, filename, type, content_len, opts = {}, headers = {})
-      trans_encoding = opts["Content-Transfer-Encoding"] || "binary"
-      content_disposition = opts["Content-Disposition"] || "form-data"
+    # @param boundary [String]
+    # @param name [#to_s]
+    # @param filename [String]
+    # @param type [String]
+    # @param content_len [Integer]
+    # @param opts [Hash]
+    def build_head(boundary, name, filename, type, content_len, opts = {})
+      opts = opts.clone
+
+      trans_encoding = opts.delete("Content-Transfer-Encoding") || "binary"
+      content_disposition = opts.delete("Content-Disposition") || "form-data"
 
       part = ''
       part << "--#{boundary}\r\n"
       part << "Content-Disposition: #{content_disposition}; name=\"#{name.to_s}\"; filename=\"#{filename}\"\r\n"
       part << "Content-Length: #{content_len}\r\n"
-      if content_id = opts["Content-ID"]
+      if content_id = opts.delete("Content-ID")
         part << "Content-ID: #{content_id}\r\n"
       end
 
-      if headers["Content-Type"] != nil
-        part <<  "Content-Type: " + headers["Content-Type"] + "\r\n"
+      if opts["Content-Type"] != nil
+        part <<  "Content-Type: " + opts["Content-Type"] + "\r\n"
       else
         part << "Content-Type: #{type}\r\n"
       end
 
       part << "Content-Transfer-Encoding: #{trans_encoding}\r\n"
+
+      opts.each do |k, v|
+        part << "#{k}: #{v}\r\n"
+      end
+
       part << "\r\n"
     end
   end
@@ -88,8 +117,9 @@ module Parts
   # Represents the epilogue or closing boundary.
   class EpiloguePart
     include Part
+
     def initialize(boundary)
-      @part = "--#{boundary}--\r\n\r\n"
+      @part = "--#{boundary}--\r\n"
       @io = StringIO.new(@part)
     end
   end
diff --git a/metadata.yml b/metadata.yml
deleted file mode 100644
index 89c68d4..0000000
--- a/metadata.yml
+++ /dev/null
@@ -1,79 +0,0 @@
---- !ruby/object:Gem::Specification
-name: multipart-post
-version: !ruby/object:Gem::Version
-  version: 2.0.0
-  prerelease: 
-platform: ruby
-authors:
-- Nick Sieger
-autorequire: 
-bindir: bin
-cert_chain: []
-date: 2013-12-21 00:00:00.000000000 Z
-dependencies: []
-description: ! 'Use with Net::HTTP to do multipart form posts.  IO values that have
-  #content_type, #original_filename, and #local_path will be posted as a binary file.'
-email:
-- nick@nicksieger.com
-executables: []
-extensions: []
-extra_rdoc_files: []
-files:
-- .gitignore
-- .travis.yml
-- Gemfile
-- History.txt
-- Manifest.txt
-- README.md
-- Rakefile
-- lib/composite_io.rb
-- lib/multipart_post.rb
-- lib/multipartable.rb
-- lib/net/http/post/multipart.rb
-- lib/parts.rb
-- multipart-post.gemspec
-- test/multibyte.txt
-- test/net/http/post/test_multipart.rb
-- test/test_composite_io.rb
-- test/test_parts.rb
-homepage: https://github.com/nicksieger/multipart-post
-licenses:
-- MIT
-post_install_message: 
-rdoc_options:
-- --main
-- README.md
-- -SHN
-- -f
-- darkfish
-require_paths:
-- lib
-required_ruby_version: !ruby/object:Gem::Requirement
-  none: false
-  requirements:
-  - - ! '>='
-    - !ruby/object:Gem::Version
-      version: '0'
-      segments:
-      - 0
-      hash: 3851181222699685043
-required_rubygems_version: !ruby/object:Gem::Requirement
-  none: false
-  requirements:
-  - - ! '>='
-    - !ruby/object:Gem::Version
-      version: '0'
-      segments:
-      - 0
-      hash: 3851181222699685043
-requirements: []
-rubyforge_project: caldersphere
-rubygems_version: 1.8.23
-signing_key: 
-specification_version: 3
-summary: A multipart form post accessory for Net::HTTP.
-test_files:
-- test/multibyte.txt
-- test/net/http/post/test_multipart.rb
-- test/test_composite_io.rb
-- test/test_parts.rb
diff --git a/multipart-post.gemspec b/multipart-post.gemspec
index 6954f09..a3377f5 100644
--- a/multipart-post.gemspec
+++ b/multipart-post.gemspec
@@ -2,21 +2,22 @@
 $:.push File.expand_path("../lib", __FILE__)
 require "multipart_post"
 
-Gem::Specification.new do |s|
-  s.name        = "multipart-post"
-  s.version     = MultipartPost::VERSION
-  s.authors     = ["Nick Sieger"]
-  s.email       = ["nick@nicksieger.com"]
-  s.homepage    = "https://github.com/nicksieger/multipart-post"
-  s.summary     = %q{A multipart form post accessory for Net::HTTP.}
-  s.license     = "MIT"
-  s.description = %q{Use with Net::HTTP to do multipart form posts.  IO values that have #content_type, #original_filename, and #local_path will be posted as a binary file.}
-
-  s.rubyforge_project = "caldersphere"
-
-  s.files         = `git ls-files`.split("\n")
-  s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
-  s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
-  s.rdoc_options = ["--main", "README.md", "-SHN", "-f", "darkfish"]
-  s.require_paths = ["lib"]
+Gem::Specification.new do |spec|
+  spec.name        = "multipart-post"
+  spec.version     = MultipartPost::VERSION
+  spec.authors     = ["Nick Sieger", "Samuel Williams"]
+  spec.email       = ["nick@nicksieger.com", "samuel.williams@oriontransfer.co.nz"]
+  spec.homepage    = "https://github.com/nicksieger/multipart-post"
+  spec.summary     = %q{A multipart form post accessory for Net::HTTP.}
+  spec.license     = "MIT"
+  spec.description = %q{Use with Net::HTTP to do multipart form postspec. IO values that have #content_type, #original_filename, and #local_path will be posted as a binary file.}
+  
+  spec.files         = `git ls-files`.split("\n")
+  spec.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
+  spec.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+  spec.require_paths = ["lib"]
+  
+  spec.add_development_dependency 'bundler', ['>= 1.3', '< 3']
+  spec.add_development_dependency 'rspec', '~> 3.4'
+  spec.add_development_dependency 'rake'
 end
diff --git a/spec/composite_io_spec.rb b/spec/composite_io_spec.rb
new file mode 100644
index 0000000..c9409b3
--- /dev/null
+++ b/spec/composite_io_spec.rb
@@ -0,0 +1,138 @@
+# Copyright, 2012, by Nick Sieger.
+# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
+# 
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+# 
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+# 
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+require 'composite_io'
+require 'stringio'
+require 'timeout'
+
+RSpec.shared_context "composite io" do
+  it "test_full_read_from_several_ios" do
+    expect(subject.read).to be == 'the quick brown fox'
+  end
+  
+  it "test_partial_read" do
+    expect(subject.read(9)).to be == 'the quick'
+  end
+
+  it "test_partial_read_to_boundary" do
+    expect(subject.read(10)).to be == 'the quick '
+  end
+
+  it "test_read_with_size_larger_than_available" do
+    expect(subject.read(32)).to be == 'the quick brown fox'
+  end
+  
+  it "test_read_into_buffer" do
+    buf = ''
+    subject.read(nil, buf)
+    expect(buf).to be == 'the quick brown fox'
+  end
+
+  it "test_multiple_reads" do
+    expect(subject.read(4)).to be == 'the '
+    expect(subject.read(4)).to be == 'quic'
+    expect(subject.read(4)).to be == 'k br'
+    expect(subject.read(4)).to be == 'own '
+    expect(subject.read(4)).to be == 'fox'
+  end
+
+  it "test_read_after_end" do
+    subject.read
+    expect(subject.read).to be == ""
+  end
+
+  it "test_read_after_end_with_amount" do
+    subject.read(32)
+    expect(subject.read(32)).to be_nil
+  end
+  
+  it "test_second_full_read_after_rewinding" do
+    subject.read
+    subject.rewind
+    expect(subject.read).to be == 'the quick brown fox'
+  end
+  
+  # Was apparently broken on JRuby due to http://jira.codehaus.org/browse/JRUBY-7109
+  it "test_compatible_with_copy_stream" do
+    target_io = StringIO.new
+    Timeout.timeout(1) do # Not sure why we need this in the spec?
+      IO.copy_stream(subject, target_io)
+    end
+    expect(target_io.string).to be == "the quick brown fox"
+  end
+end
+
+RSpec.describe CompositeReadIO do
+  describe "generic io" do
+    subject {StringIO.new('the quick brown fox')}
+  
+    include_context "composite io"
+  end
+  
+  describe "composite io" do
+    subject {CompositeReadIO.new(StringIO.new('the '), StringIO.new('quick '), StringIO.new('brown '), StringIO.new('fox'))}
+  
+    include_context "composite io"
+  end
+  
+  describe "nested composite io" do
+    subject {CompositeReadIO.new(CompositeReadIO.new(StringIO.new('the '), StringIO.new('quick ')), StringIO.new('brown '), StringIO.new('fox'))}
+  
+    include_context "composite io"
+  end
+  
+  describe "unicode composite io" do
+    let(:utf8_io) {File.open(File.dirname(__FILE__)+'/multibyte.txt')}
+    let(:binary_io) {StringIO.new("\x86")}
+    
+    subject {CompositeReadIO.new(binary_io, utf8_io)}
+    
+    it "test_read_from_multibyte" do
+      expect(subject.read).to be == "\x86\xE3\x83\x95\xE3\x82\xA1\xE3\x82\xA4\xE3\x83\xAB\n".b
+    end
+  end
+  
+  it "test_convert_error" do
+    expect do
+      UploadIO.convert!('tmp.txt', 'text/plain', 'tmp.txt', 'tmp.txt')
+    end.to raise_error(ArgumentError, /convert! has been removed/)
+  end
+  
+  it "test_empty" do
+    expect(subject.read).to be == ""
+  end
+
+  it "test_empty_limited" do
+    expect(subject.read(1)).to be_nil
+  end
+
+  it "test_empty_parts" do
+    io = CompositeReadIO.new(StringIO.new, StringIO.new('the '), StringIO.new, StringIO.new('quick'))
+    expect(io.read(3)).to be == "the"
+    expect(io.read(3)).to be == " qu"
+    expect(io.read(3)).to be == "ick"
+  end
+
+  it "test_all_empty_parts" do
+    io = CompositeReadIO.new(StringIO.new, StringIO.new)
+    expect(io.read(1)).to be_nil
+  end
+end
diff --git a/test/multibyte.txt b/spec/multibyte.txt
similarity index 100%
rename from test/multibyte.txt
rename to spec/multibyte.txt
diff --git a/test/net/http/post/test_multipart.rb b/spec/net/http/post/multipart_spec.rb
similarity index 59%
rename from test/net/http/post/test_multipart.rb
rename to spec/net/http/post/multipart_spec.rb
index c127e0a..a19ea04 100644
--- a/test/net/http/post/test_multipart.rb
+++ b/spec/net/http/post/multipart_spec.rb
@@ -5,42 +5,72 @@
 #++
 
 require 'net/http/post/multipart'
-require 'test/unit'
 
-class Net::HTTP::Post::MultiPartTest < Test::Unit::TestCase
-  TEMP_FILE = "temp.txt"
-
-  HTTPPost = Struct.new("HTTPPost", :content_length, :body_stream, :content_type)
-  HTTPPost.module_eval do
-    def set_content_type(type, params = {})
-      self.content_type = type + params.map{|k,v|"; #{k}=#{v}"}.join('')
+RSpec.shared_context "net http multipart" do
+  let(:temp_file) {"temp.txt"}
+  let(:http_post) do
+    Struct.new("HTTPPost", :content_length, :body_stream, :content_type) do
+      def set_content_type(type, params = {})
+        self.content_type = type + params.map{|k,v|"; #{k}=#{v}"}.join('')
+      end
+    end
+  end
+  
+  after(:each) do
+    File.delete(temp_file) rescue nil
+  end
+  
+  def assert_results(post)
+    expect(post.content_length).to be > 0
+    expect(post.body_stream).to_not be_nil
+    
+    expect(post['content-type']).to be == "multipart/form-data; boundary=#{post.boundary}"
+    
+    body = post.body_stream.read
+    boundary_regex = Regexp.quote(post.boundary)
+    
+    expect(body).to be =~ /1234567890/
+    
+    # ensure there is at least one boundary
+    expect(body).to be =~ /^--#{boundary_regex}\r\n/
+    
+    # ensure there is an epilogue
+    expect(body).to be =~ /^--#{boundary_regex}--\r\n/
+    expect(body).to be =~ /text\/plain/
+    
+    if (body =~ /multivalueParam/)
+      expect(body.scan(/^.*multivalueParam.*$/).size).to be == 2
     end
   end
 
-  def teardown
-    File.delete(TEMP_FILE) rescue nil
+  def assert_additional_headers_added(post, parts_headers)
+    post.body_stream.rewind
+    body = post.body_stream.read
+    parts_headers.each do |part, headers|
+      headers.each do |k,v|
+        expect(body).to be =~ /#{k}: #{v}/
+      end
+    end
   end
+end
 
-  def test_form_multipart_body
+RSpec.describe Net::HTTP::Post::Multipart do
+  include_context "net http multipart"
+  
+  it "test_form_multipart_body" do
     File.open(TEMP_FILE, "w") {|f| f << "1234567890"}
     @io = File.open(TEMP_FILE)
     @io = UploadIO.new @io, "text/plain", TEMP_FILE
     assert_results Net::HTTP::Post::Multipart.new("/foo/bar", :foo => 'bar', :file => @io)
   end
-  def test_form_multipart_body_put
-    File.open(TEMP_FILE, "w") {|f| f << "1234567890"}
-    @io = File.open(TEMP_FILE)
-    @io = UploadIO.new @io, "text/plain", TEMP_FILE
-    assert_results Net::HTTP::Put::Multipart.new("/foo/bar", :foo => 'bar', :file => @io)
-  end
 
-  def test_form_multipart_body_with_stringio
+  it "test_form_multipart_body_with_stringio" do
     @io = StringIO.new("1234567890")
     @io = UploadIO.new @io, "text/plain", TEMP_FILE
     assert_results Net::HTTP::Post::Multipart.new("/foo/bar", :foo => 'bar', :file => @io)
   end
 
-  def test_form_multiparty_body_with_parts_headers
+  it "test_form_multiparty_body_with_parts_headers" do
     @io = StringIO.new("1234567890")
     @io = UploadIO.new @io, "text/plain", TEMP_FILE
     parts = { :text => 'bar', :file => @io }
@@ -56,55 +86,38 @@ class Net::HTTP::Post::MultiPartTest < Test::Unit::TestCase
     assert_additional_headers_added(request, headers[:parts])
   end
 
-  def test_form_multipart_body_with_array_value
+  it "test_form_multipart_body_with_array_value" do
     File.open(TEMP_FILE, "w") {|f| f << "1234567890"}
     @io = File.open(TEMP_FILE)
     @io = UploadIO.new @io, "text/plain", TEMP_FILE
     params = {:foo => ['bar', 'quux'], :file => @io}
     headers = { :parts => {
         :foo => { "Content-Type" => "application/json; charset=UTF-8" } } }
-    post = Net::HTTP::Post::Multipart.new("/foo/bar", params, headers,
-                                          Net::HTTP::Post::Multipart::DEFAULT_BOUNDARY)
-
-    assert post.content_length && post.content_length > 0
-    assert post.body_stream
+    post = Net::HTTP::Post::Multipart.new("/foo/bar", params, headers)
+    
+    expect(post.content_length).to be > 0
+    expect(post.body_stream).to_not be_nil
 
     body = post.body_stream.read
-    assert_equal 2, body.lines.grep(/name="foo"/).length
-    assert body =~ /Content-Type: application\/json; charset=UTF-8/, body
+    expect(body.lines.grep(/name="foo"/).length).to be == 2
+    expect(body).to be =~ /Content-Type: application\/json; charset=UTF-8/
   end
 
-  def test_form_multipart_body_with_arrayparam
+  it "test_form_multipart_body_with_arrayparam" do
     File.open(TEMP_FILE, "w") {|f| f << "1234567890"}
     @io = File.open(TEMP_FILE)
     @io = UploadIO.new @io, "text/plain", TEMP_FILE
     assert_results Net::HTTP::Post::Multipart.new("/foo/bar", :multivalueParam => ['bar','bah'], :file => @io)
   end
+end
 
-  def assert_results(post)
-    assert post.content_length && post.content_length > 0
-    assert post.body_stream
-    assert_equal "multipart/form-data; boundary=#{Multipartable::DEFAULT_BOUNDARY}", post['content-type']
-    body = post.body_stream.read
-    boundary_regex = Regexp.quote Multipartable::DEFAULT_BOUNDARY
-    assert body =~ /1234567890/
-    # ensure there is at least one boundary
-    assert body =~ /^--#{boundary_regex}\r\n/
-    # ensure there is an epilogue
-    assert body =~ /^--#{boundary_regex}--\r\n/
-    assert body =~ /text\/plain/
-    if (body =~ /multivalueParam/)
-    	assert_equal 2, body.scan(/^.*multivalueParam.*$/).size
-    end
-  end
-
-  def assert_additional_headers_added(post, parts_headers)
-    post.body_stream.rewind
-    body = post.body_stream.read
-    parts_headers.each do |part, headers|
-      headers.each do |k,v|
-        assert body =~ /#{k}: #{v}/
-      end
-    end
+RSpec.describe Net::HTTP::Put::Multipart do
+  include_context "net http multipart"
+  
+  it "test_form_multipart_body_put" do
+    File.open(TEMP_FILE, "w") {|f| f << "1234567890"}
+    @io = File.open(TEMP_FILE)
+    @io = UploadIO.new @io, "text/plain", TEMP_FILE
+    assert_results Net::HTTP::Put::Multipart.new("/foo/bar", :foo => 'bar', :file => @io)
   end
 end
diff --git a/spec/parts_spec.rb b/spec/parts_spec.rb
new file mode 100644
index 0000000..ceaab6e
--- /dev/null
+++ b/spec/parts_spec.rb
@@ -0,0 +1,102 @@
+# Copyright, 2012, by Nick Sieger.
+# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+require 'parts'
+require 'stringio'
+require 'composite_io'
+require 'tempfile'
+
+MULTIBYTE = File.dirname(__FILE__)+'/multibyte.txt'
+TEMP_FILE = "temp.txt"
+
+module AssertPartLength
+  def assert_part_length(part)
+    bytes = part.to_io.read
+    bytesize = bytes.respond_to?(:bytesize) ? bytes.bytesize : bytes.length
+    expect(bytesize).to be == part.length
+  end
+end
+
+RSpec.describe Parts do
+  let(:string_with_content_type) do
+    Class.new(String) do
+      def content_type; 'application/data'; end
+    end
+  end
+
+  it "test_file_with_upload_io" do
+    expect(Parts::Part.file?(UploadIO.new(__FILE__, "text/plain"))).to be true
+  end
+
+  it "test_file_with_modified_string" do
+    expect(Parts::Part.file?(string_with_content_type.new("Hello"))).to be false
+  end
+
+  it "test_new_with_modified_string" do
+    expect(Parts::Part.new("boundary", "multibyte", string_with_content_type.new("Hello"))).to be_kind_of(Parts::ParamPart)
+  end
+end
+
+RSpec.describe Parts::FilePart do
+  include AssertPartLength
+
+  before(:each) do
+    File.open(TEMP_FILE, "w") {|f| f << "1234567890"}
+    io =  UploadIO.new(TEMP_FILE, "text/plain")
+    @part = Parts::FilePart.new("boundary", "afile", io)
+  end
+
+  after(:each) do
+    File.delete(TEMP_FILE) rescue nil
+  end
+
+  it "test_correct_length" do
+    assert_part_length @part
+  end
+
+  it "test_multibyte_file_length" do
+    assert_part_length Parts::FilePart.new("boundary", "multibyte", UploadIO.new(MULTIBYTE, "text/plain"))
+  end
+
+  it "test_multibyte_filename" do
+    name = File.read(MULTIBYTE, 300)
+    file = Tempfile.new(name.respond_to?(:force_encoding) ? name.force_encoding("UTF-8") : name)
+    assert_part_length Parts::FilePart.new("boundary", "multibyte", UploadIO.new(file, "text/plain"))
+    file.close
+  end
+
+   it "test_force_content_type_header" do
+    part = Parts::FilePart.new("boundary", "afile", UploadIO.new(TEMP_FILE, "text/plain"), { "Content-Type" => "application/pdf" })
+    expect(part.to_io.read).to match(/Content-Type: application\/pdf/)
+  end
+end
+
+RSpec.describe Parts::ParamPart do
+  include AssertPartLength
+
+  before(:each) do
+    @part = Parts::ParamPart.new("boundary", "multibyte", File.read(MULTIBYTE))
+  end
+
+  it "test_correct_length" do
+    assert_part_length @part
+  end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..c52cf78
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,29 @@
+
+if ENV['COVERAGE']
+	begin
+		require 'simplecov'
+		
+		SimpleCov.start do
+			add_filter "/spec/"
+		end
+		
+		if ENV['TRAVIS']
+			require 'coveralls'
+			Coveralls.wear!
+		end
+	rescue LoadError
+		warn "Could not load simplecov: #{$!}"
+	end
+end
+
+require "bundler/setup"
+require "multipart_post"
+
+RSpec.configure do |config|
+	# Enable flags like --only-failures and --next-failure
+	config.example_status_persistence_file_path = ".rspec_status"
+
+	config.expect_with :rspec do |c|
+		c.syntax = :expect
+	end
+end
diff --git a/test/test_composite_io.rb b/test/test_composite_io.rb
deleted file mode 100644
index 6e8a193..0000000
--- a/test/test_composite_io.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-#--
-# Copyright (c) 2007-2013 Nick Sieger.
-# See the file README.txt included with the distribution for
-# software license details.
-#++
-
-require 'composite_io'
-require 'stringio'
-require 'test/unit'
-require 'timeout'
-
-class CompositeReadIOTest < Test::Unit::TestCase
-  def setup
-    @io = CompositeReadIO.new(CompositeReadIO.new(StringIO.new('the '), StringIO.new('quick ')),
-            StringIO.new('brown '), StringIO.new('fox'))
-  end
-
-  def test_full_read_from_several_ios
-    assert_equal 'the quick brown fox', @io.read
-  end
-
-  unless RUBY_VERSION < '1.9'
-    def test_read_from_multibyte
-      utf8    = File.open(File.dirname(__FILE__)+'/multibyte.txt')
-      binary  = StringIO.new("\x86")
-      @io = CompositeReadIO.new(binary,utf8)
-
-      expect  = "\x86\xE3\x83\x95\xE3\x82\xA1\xE3\x82\xA4\xE3\x83\xAB\n"
-      expect.force_encoding('BINARY') if expect.respond_to?(:force_encoding)
-      assert_equal expect, @io.read
-    end
-  end
-
-  def test_partial_read
-    assert_equal 'the quick', @io.read(9)
-  end
-
-  def test_partial_read_to_boundary
-    assert_equal 'the quick ', @io.read(10)
-  end
-
-  def test_read_with_size_larger_than_available
-    assert_equal 'the quick brown fox', @io.read(32)
-  end
-
-  def test_read_into_buffer
-    buf = ''
-    @io.read(nil, buf)
-    assert_equal 'the quick brown fox', buf
-  end
-
-  def test_multiple_reads
-    assert_equal 'the ', @io.read(4)
-    assert_equal 'quic', @io.read(4)
-    assert_equal 'k br', @io.read(4)
-    assert_equal 'own ', @io.read(4)
-    assert_equal 'fox',  @io.read(4)
-  end
-
-  def test_read_after_end
-    @io.read
-    assert_equal "", @io.read
-  end
-
-  def test_read_after_end_with_amount
-    @io.read(32)
-    assert_equal nil, @io.read(32)
-  end
-  
-  def test_second_full_read_after_rewinding
-    @io.read
-    @io.rewind
-    assert_equal 'the quick brown fox', @io.read
-  end
-
-  def test_convert_error
-    assert_raises(ArgumentError) {
-      UploadIO.convert!('tmp.txt', 'text/plain', 'tmp.txt', 'tmp.txt')
-    }
-  end
-
-  ## FIXME excluding on JRuby due to
-  ## http://jira.codehaus.org/browse/JRUBY-7109
-  if IO.respond_to?(:copy_stream) && !defined?(JRUBY_VERSION)
-    def test_compatible_with_copy_stream
-      target_io = StringIO.new
-      Timeout.timeout(1) do
-        IO.copy_stream(@io, target_io)
-      end
-      assert_equal "the quick brown fox", target_io.string
-    end
-  end
-
-  def test_empty
-    io = CompositeReadIO.new
-    assert_equal "", io.read
-  end
-
-  def test_empty_limited
-    io = CompositeReadIO.new
-    assert_nil io.read(1)
-  end
-
-  def test_empty_parts
-    io = CompositeReadIO.new(StringIO.new, StringIO.new('the '), StringIO.new, StringIO.new('quick'))
-    assert_equal "the", io.read(3)
-    assert_equal " qu", io.read(3)
-    assert_equal "ick", io.read(4)
-  end
-
-  def test_all_empty_parts
-    io = CompositeReadIO.new(StringIO.new, StringIO.new)
-    assert_nil io.read(1)
-  end
-end
diff --git a/test/test_parts.rb b/test/test_parts.rb
deleted file mode 100644
index 33c1e39..0000000
--- a/test/test_parts.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-#--
-# Copyright (c) 2007-2012 Nick Sieger.
-# See the file README.txt included with the distribution for
-# software license details.
-#++
-
-require 'test/unit'
-
-require 'parts'
-require 'stringio'
-require 'composite_io'
-require 'tempfile'
-
-
-MULTIBYTE = File.dirname(__FILE__)+'/multibyte.txt'
-TEMP_FILE = "temp.txt"
-
-module AssertPartLength
-  def assert_part_length(part)
-    bytes = part.to_io.read
-    bytesize = bytes.respond_to?(:bytesize) ? bytes.bytesize : bytes.length
-    assert_equal bytesize, part.length
-  end
-end
-
-class PartTest < Test::Unit::TestCase
-  def setup
-    @string_with_content_type = Class.new(String) do
-      def content_type; 'application/data'; end
-    end
-  end
-
-  def test_file_with_upload_io
-    assert Parts::Part.file?(UploadIO.new(__FILE__, "text/plain"))
-  end
-
-  def test_file_with_modified_string
-    assert !Parts::Part.file?(@string_with_content_type.new("Hello"))
-  end
-
-  def test_new_with_modified_string
-    assert_kind_of Parts::ParamPart,
-      Parts::Part.new("boundary", "multibyte", @string_with_content_type.new("Hello"))
-  end
-end
-
-class FilePartTest < Test::Unit::TestCase
-  include AssertPartLength
-
-  def setup
-    File.open(TEMP_FILE, "w") {|f| f << "1234567890"}
-    io =  UploadIO.new(TEMP_FILE, "text/plain")
-    @part = Parts::FilePart.new("boundary", "afile", io)
-  end
-
-  def teardown
-    File.delete(TEMP_FILE) rescue nil
-  end
-
-  def test_correct_length
-    assert_part_length @part
-  end
-
-  def test_multibyte_file_length
-    assert_part_length Parts::FilePart.new("boundary", "multibyte", UploadIO.new(MULTIBYTE, "text/plain"))
-  end
-
-  def test_multibyte_filename
-    name = File.read(MULTIBYTE, 300)
-    file = Tempfile.new(name.respond_to?(:force_encoding) ? name.force_encoding("UTF-8") : name)
-    assert_part_length Parts::FilePart.new("boundary", "multibyte", UploadIO.new(file, "text/plain"))
-    file.close
-  end
-end
-
-class ParamPartTest < Test::Unit::TestCase
-  include AssertPartLength
-
-  def setup
-    @part = Parts::ParamPart.new("boundary", "multibyte", File.read(MULTIBYTE))
-  end
-
-  def test_correct_length
-    assert_part_length @part
-  end
-end