diff --git a/btrfs.go b/btrfs.go
index aef7e10..a99204b 100644
--- a/btrfs.go
+++ b/btrfs.go
@@ -1,6 +1,7 @@
 package btrfs
 
 import (
+	"bytes"
 	"fmt"
 	"github.com/dennwc/btrfs/ioctl"
 	"io"
@@ -228,6 +229,8 @@ const (
 	ZLIB            = Compression("zlib")
 )
 
+const xattrCompression = xattrPrefix + "compression"
+
 func SetCompression(path string, v Compression) error {
 	var value []byte
 	if v != CompressionNone {
@@ -237,9 +240,39 @@ func SetCompression(path string, v Compression) error {
 			return err
 		}
 	}
-	err := syscall.Setxattr(path, xattrPrefix+"compression", value, 0)
+	err := syscall.Setxattr(path, xattrCompression, value, 0)
 	if err != nil {
 		return &os.PathError{Op: "setxattr", Path: path, Err: err}
 	}
 	return nil
 }
+
+func GetCompression(path string) (Compression, error) {
+	var buf []byte
+	for {
+		sz, err := syscall.Getxattr(path, xattrCompression, nil)
+		if err == syscall.ENODATA || sz == 0 {
+			return CompressionNone, nil
+		} else if err != nil {
+			return CompressionNone, &os.PathError{Op: "getxattr", Path: path, Err: err}
+		}
+		if cap(buf) < sz {
+			buf = make([]byte, sz)
+		} else {
+			buf = buf[:sz]
+		}
+		sz, err = syscall.Getxattr(path, xattrCompression, buf)
+		if err == syscall.ENODATA {
+			return CompressionNone, nil
+		} else if err == syscall.ERANGE {
+			// xattr changed by someone else, and is larger than our current buffer
+			continue
+		} else if err != nil {
+			return CompressionNone, &os.PathError{Op: "getxattr", Path: path, Err: err}
+		}
+		buf = buf[:sz]
+		break
+	}
+	buf = bytes.TrimSuffix(buf, []byte{0})
+	return Compression(buf), nil
+}
diff --git a/btrfs_test.go b/btrfs_test.go
new file mode 100644
index 0000000..e09abe1
--- /dev/null
+++ b/btrfs_test.go
@@ -0,0 +1,97 @@
+package btrfs
+
+import (
+	"github.com/dennwc/btrfs/test"
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+const sizeDef = 256 * 1024 * 1024
+
+func TestOpen(t *testing.T) {
+	dir, closer := btrfstest.New(t, sizeDef)
+	defer closer()
+	fs, err := Open(dir, true)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err = fs.Close(); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestIsSubvolume(t *testing.T) {
+	dir, closer := btrfstest.New(t, sizeDef)
+	defer closer()
+
+	isSubvol := func(path string, expect bool) {
+		ok, err := IsSubVolume(path)
+		if err != nil {
+			t.Errorf("failed to check subvolume %v: %v", path, err)
+			return
+		} else if ok != expect {
+			t.Errorf("unexpected result for %v", path)
+		}
+	}
+	mkdir := func(path string) {
+		path = filepath.Join(dir, path)
+		if err := os.MkdirAll(path, 0755); err != nil {
+			t.Fatalf("cannot create dir %v: %v", path, err)
+		}
+		isSubvol(path, false)
+	}
+
+	mksub := func(path string) {
+		path = filepath.Join(dir, path)
+		if err := CreateSubVolume(path); err != nil {
+			t.Fatalf("cannot create subvolume %v: %v", path, err)
+		}
+		isSubvol(path, true)
+	}
+
+	mksub("v1")
+
+	mkdir("v1/d2")
+	mksub("v1/v2")
+
+	mkdir("v1/d2/d3")
+	mksub("v1/d2/v3")
+
+	mkdir("v1/v2/d3")
+	mksub("v1/v2/v3")
+
+	mkdir("d1")
+
+	mkdir("d1/d2")
+	mksub("d1/v2")
+
+	mkdir("d1/d2/d3")
+	mksub("d1/d2/v3")
+
+	mkdir("d1/v2/d3")
+	mksub("d1/v2/v3")
+}
+
+func TestCompression(t *testing.T) {
+	dir, closer := btrfstest.New(t, sizeDef)
+	defer closer()
+	fs, err := Open(dir, true)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer fs.Close()
+	if err := fs.CreateSubVolume("sub"); err != nil {
+		t.Fatal(err)
+	}
+	path := filepath.Join(dir, "sub")
+
+	if err := SetCompression(path, LZO); err != nil {
+		t.Fatal(err)
+	}
+	if c, err := GetCompression(path); err != nil {
+		t.Fatal(err)
+	} else if c != LZO {
+		t.Fatalf("unexpected compression returned: %q", string(c))
+	}
+}
diff --git a/test/btrfstest.go b/test/btrfstest.go
new file mode 100644
index 0000000..bb1cced
--- /dev/null
+++ b/test/btrfstest.go
@@ -0,0 +1,106 @@
+package btrfstest
+
+import (
+	"bytes"
+	"errors"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"strings"
+	"testing"
+	"time"
+)
+
+func run(name string, args ...string) error {
+	buf := bytes.NewBuffer(nil)
+	cmd := exec.Command(name, args...)
+	cmd.Stdout = buf
+	cmd.Stderr = buf
+	err := cmd.Run()
+	if err == nil {
+		return nil
+	} else if buf.Len() == 0 {
+		return err
+	}
+	return errors.New("error: " + strings.TrimSpace(string(buf.Bytes())))
+}
+
+func Mkfs(file string, size int64) error {
+	f, err := os.Create(file)
+	if err != nil {
+		return err
+	}
+	if err = f.Truncate(size); err != nil {
+		f.Close()
+		return err
+	}
+	if err = f.Close(); err != nil {
+		return err
+	}
+	if err = run("mkfs.btrfs", file); err != nil {
+		os.Remove(file)
+		return err
+	}
+	return err
+}
+
+func Mount(mount string, file string) error {
+	if err := run("mount", file, mount); err != nil {
+		return err
+	}
+	return nil
+}
+
+func New(t testing.TB, size int64) (string, func()) {
+	f, err := ioutil.TempFile("", "btrfs_vol")
+	if err != nil {
+		t.Fatal(err)
+	}
+	name := f.Name()
+	f.Close()
+	rm := func() {
+		os.Remove(name)
+	}
+	if err = Mkfs(name, size); err != nil {
+		rm()
+	}
+	mount, err := ioutil.TempDir("", "btrfs_mount")
+	if err != nil {
+		rm()
+		t.Fatal(err)
+	}
+	if err = Mount(mount, name); err != nil {
+		rm()
+		os.RemoveAll(mount)
+		if txt := err.Error(); strings.Contains(txt, "permission denied") ||
+			strings.Contains(txt, "only root") {
+			t.Skip(err)
+		} else {
+			t.Fatal(err)
+		}
+	}
+	done := false
+	return mount, func() {
+		if done {
+			return
+		}
+		for i := 0; i < 5; i++ {
+			if err := run("umount", mount); err == nil {
+				break
+			} else {
+				log.Println("umount failed:", err)
+				if strings.Contains(err.Error(), "busy") {
+					time.Sleep(time.Second)
+				} else {
+					break
+				}
+			}
+		}
+		if err := os.Remove(mount); err != nil {
+			log.Println("cleanup failed:", err)
+		}
+		rm()
+		done = true
+	}
+}