Archive

Posts Tagged ‘TuneWiz’

TuneWiz Loosely Coupled, Easily Testable

January 13th, 2009 No comments

A few years ago someone introduced me to the idea of unit testing with NUnit.  Brilliant!  Whenever I develop applications I inevitably write a lot of throw away code.  Lots of code to verify assumptions.  I would simply delete or comment out this code.  With unit testing, I keep all this code and I re-run it to verify any changes I’ve made haven’t invalidated my assumptions.  A while later, another light bulb lit up for me.  These ‘unit tests’ I’d been writing were not unit tests at all!  They were integration tests.  Sure I would test the class in question, but I would create any supporting objects along the way.  At first this double testing of objects seemed like over-engineering, but at some point I realized I was creating a lot of redundency.  Introduce a database into the equation and now I’m doing more testing than necessary and my tests are running slowly because I’m making round trips to a database.   If I want to test business logic, it makes sense that I would want to separate this from testing whether or not I can read and write to a database.

I’m still learning proper unit testing.  While not specific to unit testing, there is in the community at large,  the helpful concept of dependency injection or inversion of control.  One aspect I’ve latched onto is instead of creating ‘new’ objects in a class, the class should take these objects as parameters passed in to your constructor or methods.  Your methods or constructors expect an interface and you leave it up the caller what implementation you want to push in.   Even if you only have one concrete implementation of your interface, this leaves the door open to to create a fake implementation for your unit tests.  Or you can use a mocking framework like Moq or Rhino Mocks to help you do this.   So what this helps you to do is create code that tests a single unit of functionaltiy in isolation.

So now that we have the background out of the way, let’s get to how this applies to TuneWiz.

I have an object called TuneTrack that contains your basic track information.  I created a TuneTrack collection to hold a bunch of these tracks.  I decided to add a GetOrphanTracks method to this collection so I could get all the tracks that iTunes lists in it’s library, but don’t exist on disk.   I can tell a track is an orphan if the Location property of it is null or if it doesn’t exist on disk.  Simple enough. Here’s my first unit test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Test]
public void Return_orphan_track_with_location_null()
{
	// Mock TuneTrack created with Moq library
	var mock = new Mock<TuneTrack>(null);
	// Compiler doesnt like it when we simply pass in bare null for Returns
	string nullStr = null;
	// When something accesses our mock implementation's Locaion
	// property, we make it return null
	mock.Setup(x => x.Location).Returns(nullStr);
 
	// Create the TuneCache object we're unit testing
	var tc = new TuneCache();
	// Add the mock TuneTrack to the collection
	tc.Add(mock.Object);
 
	// Make sure we have a 'real' tunetrack
	Assert.IsNotNull(tc.GetOrphanTracks().First());
	// Location of track should be null as reported by our mock object
	Assert.IsNull(tc.GetOrphanTracks().First().Location);
}

If a TuneTrack’s Location property is null, that means it definitely doesn’t exist on disk. Excellent.

Here’s the method in the TuneCache object that makes that test pass:

1
2
3
4
5
6
7
8
9
10
public IEnumerable<TuneTrack> GetOrphanTracks()
{
	// LINQ statement to return orphan tracks
	// In my experience, iTunes seems to return a null when the file doesnt exist on disk
	var orphanTracks = from t in this
				 where t.Location == null
				 select t;
 
	return orphanTracks;
}

Next test we want to create a mock TuneTrack with a real location that does not exist on disk. Here’s where it gets tricky. The old me that didn’t know the difference between an integration test and a unit test would create a TestData directory, fill it with some files and write some tests against those files. Some calls to our static friend File.Exists and whiz bang we’re done! I remind myself I just want to test the TuneCache object. I don’t want to test the File.Exists method.

This is where that IoC stuff comes in. I did some searching to see what other people do about File.Exists type work in regards to testing. Create an interface and in my concrete implementation create a non-static method that calls File.Exists. That seemed to gel with advice that static methods and singletons might be harmful for testing. So I need to refactor my GetOrphanTracks method to take my new IFileTools interface. The big picture here is that I can swap in a FAKE implementation that can be used to simulate files existing or not existing. Here is my one of my unit tests for this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[Test]
public void Return_one_orphan_track_where_location_does_not_exist()
{
	// Mock TuneTrack created with Moq library
	var mockTune = new Mock<TuneTrack>(null);
	// This file probably exists on most systems
	// it doesn't matter though because it's fake
	// no files are harmed or accessed in this method
	var notepad = "C:\\windows\\notepad.exe";
	// when the location of our mockTune is accessed
	// the location will be notepad
	mockTune.Setup(x => x.Location).Returns(notepad);
 
	// Create our real TuneCache
	var tc = new TuneCache();
	// Add our fake tunetrack
	tc.Add(mockTune.Object);
 
	// Create a fake IFileTools that will simulate
	// a file not existing.  This isolates the test
	// and removes the dependency on the file system and File.Exists()
	var fakeFileTools = new Mock<IFileTools>();
	// any call to exists return false
	fakeFileTools.Setup(x => x.Exists(It.IsAny<string>())).Returns(false);
 
	// Get list of orphan tracks using our mock FileTools that will tell
	// us no file exists
	var orphanTracks = tc.GetOrphanTracks(fakeFileTools.Object); 
	Assert.IsTrue(orphanTracks.Count() == 1, "Cache did not contain 1 orphan track");
	Assert.AreEqual(notepad, orphanTracks.First().Location, "TuneTrack did not have expected location on disk");
}

Now let’s look at the new method to make this test pass:

1
2
3
4
5
6
7
8
9
10
11
public IEnumerable<TuneTrack> GetOrphanTracks(IFileTools fileTools)
{
	// LINQ statement to return orphan tracks
	// In my experience, iTunes seems to return a null when the file doesnt exist on disk
	// I believe iTunes may perform it's own Exist, but we'll be extra safe and do our own
	var orphanTracks = from t in this
				 where t.Location == null || fileTools.Exists(t.Location) == false
				 select t;
 
	return orphanTracks;
}

The one thing that feels awkward to me is now in my real code I have to pass in a new ConcreteFileTools() object when I call my get orphans method. It just feels awkward to me. I know there are tools to help with this, but I’m not ready to pick an IoC library. I don’t feel like things are complex enough to warrant it. I was just read this article, Dependency Injection in the Real World. It gave me an idea:

1
2
3
4
public IEnumerable<TuneTrack> GetOrphanTracks()
{
	return GetOrphanTracks(new FileTools());
}

Bang! Now in the ‘real’ world, I don’t have to think about it. I only worry about injecting an IFileTools in test isolation land. Now if I find I need multiple concrete implementations of IFileTools, I will probably switch my approach. But now my unit tests pass and TuneCache doesn’t feel too encumbered. On the other hand, wasn’t I trying to get away from new? Perhaps this is a a hybrid approach for easing slowly into the IoC world.

I have checked the code in for this. Go take a look at the files for the above implementation.

Categories: Uncategorized Tags: , , , , ,

Bazaar TuneWiz Launchpad

January 11th, 2009 No comments

Professionally I have been an administrator for CVSNT, Perforce, Vault, and Subversion.   In my experience, the one area all these systems seem to fall flat is renaming when combined with merging/branching.  All of those systems start to look long in the tooth when you see the new wave of source control systems.  What appeals to me about the distributed source systems is not the P2P type repository, it’s that branching and merging are first class citizens (See Renaming is the killer app of distributed version control ).   The three big players I’ve been watching are Git, Hg (Mercurial), and Bazaar.  All of these systems seem to allow much better branching and merging.

Bazaar’s sell sheet has persuaded me to give it a spin.  In the future I would definitely like to give both Git and Mercurial some real world projects to manage.  Although I can compare and contrast feature sheets, sometimes nothing beats first hand experience.

Since I chose Bazaar as my source control system, I’ve decided to host the TuneWiz development on Launchpad.   Launchpad uses Bazaar as it’s version control system.  Launchpad has tools for managing specifications, bugs, Q&A, etc.  One of the most interesting thing Launchpad offers is the ability for anyone to easily contribute to your project.   From here:

With Launchpad and Bazaar, contributors can create their own branch of your code, make their changes and then push it all back up to Launchpad to be listed right alongside your official branches. And because they never touch your trunk they don’t need to ask for commit access.

Launchpad’s code review and Bazaar’s superb support for merging make bringing the new code and its revision history back into your branch quick and easy.

Here is TuneWiz hosted on Launchpad.

Categories: Uncategorized Tags: , , ,

New Application TuneWiz

January 11th, 2009 No comments

I’ve started a new project called TuneWiz.

Problem: I have a very specific way I  like to process and manage my music files.  I’m warming on the iTunes application, but it doesn’t always work very well with the way I like to manage my music.  My plan is to write an application to address the things that bother me.

Problems:

  1. iTunes does not monitor folders.  If I add a new folder of music, I have to manually add it.
  2. If I move or delete files on disk, iTunes doesn’t remove them from the library.
  3. iTunes stores rating information in it’s database.  I’ve been using Windows Media Player for a while and it stores rating inforation in the mp3 files.  I prefer this method because I can easily move and share music between computers and the ratings go with it.

Solution:

An application that integrates with iTunes.  Adds new files, removes orphans, and syncs rating in iTunes and id3v2 tags.  I have already written code to get comfortable with the iTunes COM interface as well as experiment with some implementation ideas.

Implementation Ideas:

  • One screen for finding orphan files
  • One screen for adding new files.  User adds folders to scan for files
  • One screen to sync rating information.
    • Sync behavior will be to take tracks rated in iTunes and save those ratings to files that are have no rating.
    • Sync behavior will also take files that have ratings and push those ratings to tracks without ratings in iTunes
    • If ratings exist in iTunes and on disk, user will be prompted which rating they want to keep
    • Future enhancement of sync behavior may include implementing a database to store historical information.  This would make rating conflicts easier to solve.

Other applications of note:

Some other applications already exist to do the things I want.  I am doing this as an exercise, but it’s worth noting other application that exist.

iTunes Library Updated – This application solves the first two problems, but it looks like the original developer isn’t sure he’s going to continue development.  It is a GPL project so source is available.

iTunes Folder Watch – This app also solves my first two problems.  It even has the ability to monitor folders.  No source is available.  Some features require a payed license.   Free version has a nag screen.

MusicBridge – This app solves the third problem.  It goes above and beyond and can sync more than just ratings.  No source is available, but app is free.

Categories: Uncategorized Tags: , ,