QuickJUnitをいじってみた

現場の既存のJUnitのテストケースがJUnit3で書かれている上、ワーキングディレクトリを変更しないとうまく動いてくれない作りになっていたため、土日で外部ファイルから指定できるようにQuickJUnitをいじってみた。ちなみに、QuickJUnitは0.5でプロジェクトごとにランチャーの設定が出来るようになるので、そちらを利用した方がよいと思われる。ただし、0.5はEclipse3.3以降でなければ利用できないので、*1もしEclipse3.2でテストランナーやワーキングディレクトリを指定したい人がいれば、このページの内容を参考にしてもらえればと思う。ちなみに、現場のEclipseのバージョンは3.2なので、今日早速入れてみたところ無事動いていくれてとても気持ちよかったw
テストランナーを指定したい理由は、JUnit3のテストをJUnit4のテストランナーで起動するとメソッド単位で起動しても全テストメソッドが実行されてしまうため。対処方法としては一度起動した後に、起動構成を開いてテストランナーをJUni4からJUnit3に下げればいいのだけど、あまりに面倒!リズムが崩壊しますがな。ちなみに、EclipseのヘルプにはJava5以降はJUnit4として判断すると書かれていた。。せめてプロジェクト単位で指定できるようにしてくれてもいい気がするのだけど。
あと、ワーキングディレクトリの指定に関しては、既存のテストコード群がなぜかデフォルトでは動かない作りになっていたので、こちらも毎回指定するのが面倒なのでまとめて指定できるようにした。

Eclipse3.2でのいじりかた

  1. ソースの入手 http://github.com/kompiro/quick-junit/zipball/REL-0_4_0 githubはzip形式でも落とせていい感じ。downloadボタンを押せばバージョンを指定して落とせる。sourceforge.jpはtar.gzでしか落とせなかった。
  2. 解凍してEclipseにインポート
  3. Required Plug-insに以下を追加する。
  4. MyJUnitLaunchShortcutクラスを追加する。
  5. ExtensionSupportクラスのcreateJUnitLaunchShortcutメソッドでMyJUnitLaunchShortcutを利用するように変更する。
  6. 動作確認をするならばそのままEclipseアプリケーションとして起動する。
  7. 自分のEclipseに組み込むならば Deployable plug-ins and fragments としてexportする。
JUnitLaunchAction

やりたいことは、QuickJUnit経由でJUnitテストケースを起動する際にテストランナーとワーキングディレクトリを指定できるようにしたい。そこでまずQuickJUnitがどこでJUnitのランチャーを起動しているのか調べてみた。すると、JUnitLaunchActionクラスのrunメソッド内でILaunchShortcutというインタフェースを経由して起動していることが分かった。

public class JUnitLaunchAction extends QuickJUnitAction {
    private ILaunchShortcut launchShortcut;
    public void run(IAction action) {
        try {
            IJavaElement element = getTargetElement(action);
            if (element == null)
                return;
            ISelection sel = new StructuredSelection(new Object[] { element });
            launchShortcut.launch(sel, mode);
        } catch (JavaModelException e) {
            QuickJUnitPlugin.getDefault().handleSystemError(e, this);
        }
    }
}
ExtensionSupport

で、ILaunchShortcutインタフェースの実体はExtensionSupportクラスのcreateJUnitLaunchShortcutメソッドを通して生成されていて、ここもインタフェースしか書かれていないので実体は分からない。結局、デバッグモードで実行して、org.eclipse.jdt.internal.junit.launcher.JUnitLaunchShortcutクラスだということが分かった。ちなみに、JUnitLaunchShortcutクラスをILaunchShortcutインタフェースとして扱うことには意味があるようで、JUnitLaunchShortcutクラスはinternalなクラスなのでEclipseバージョンに依存してしまう。そのため、このようにインタフェースで取得する必要があるのだと思われる。実際、Eclipse3.2と3.5ではJUnitLaunchShortcutのパッケージが異なっており、3.5では公開パッケージに移動していた。

public class ExtensionSupport {
    public static ILaunchShortcut createJUnitLaunchShortcut() throws CoreException {
        return createLaunchShortcut("org.eclipse.jdt.junit"); //$NON-NLS-1$
    }
    protected static ILaunchShortcut createLaunchShortcut(final String namespace)
            throws CoreException {
        final IExtensionRegistry reg = Platform.getExtensionRegistry();
        final IExtensionPoint point = reg.getExtensionPoint("org.eclipse.debug.ui.launchShortcuts"); //$NON-NLS-1$
        final IExtension[] extensions = point.getExtensions();
        for (int i = 0; i < extensions.length; ++i) {
            if (namespace.equals(extensions[i].getNamespaceIdentifier())) {
                final IConfigurationElement[] elements = extensions[i].getConfigurationElements();
                ILaunchShortcut shortcut = (ILaunchShortcut) elements[0]
                        .createExecutableExtension("class"); //$NON-NLS-1$
                if (shortcut != null) {
                    return shortcut;
                }
            }
        }
        throw new RuntimeException("LaunchShortcut not found. namespace:" + namespace); //$NON-NLS-1$
    }
}
JUnitLaunchShortcut

次にJUnitLaunchShortcutクラスのソースを見たら、ランチャーの設定を行っているっぽいソースがあったので、サブクラスを作って追加の設定を行えるようにすることにした。

public class JUnitLaunchShortcut implements ILaunchShortcut {
	public ILaunchConfiguration createConfiguration(JUnitLaunchDescription description) {
		String mainType= description.getAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME);
		String testName= description.getAttribute(JUnitBaseLaunchConfiguration.TESTNAME_ATTR);
		ILaunchConfiguration config= createConfiguration(description.getProject(), description.getName(), mainType, description.getContainer(), testName);
		
		try {
			ILaunchConfigurationWorkingCopy wc= config.getWorkingCopy();
			String testKind= description.getAttribute(JUnitBaseLaunchConfiguration.TEST_KIND_ATTR);
			wc.setAttribute(JUnitBaseLaunchConfiguration.TEST_KIND_ATTR, testKind);
			config= wc.doSave();
		} catch (CoreException ce) {
			JUnitPlugin.log(ce);
		}
		return config;
	}

	/*
	 * Overridden by org.eclipse.pde.internal.ui.launcher.JUnitWorkbenchShortcut; don't remove!
	 */
	protected ILaunchConfiguration createConfiguration(IJavaProject project, String name, String mainType, String container, String testName) {
		ILaunchConfiguration config= null;
		try {
			ILaunchConfigurationWorkingCopy wc= newWorkingCopy(name);
			wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, mainType);
			wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, project.getElementName());
			wc.setAttribute(JUnitBaseLaunchConfiguration.ATTR_KEEPRUNNING, false);
			wc.setAttribute(JUnitBaseLaunchConfiguration.LAUNCH_CONTAINER_ATTR, container);
			if (testName.length() > 0)
				wc.setAttribute(JUnitBaseLaunchConfiguration.TESTNAME_ATTR, testName);	
			AssertionVMArg.setArgDefault(wc);
			config= wc.doSave();		
		} catch (CoreException ce) {
			JUnitPlugin.log(ce);
		}
		return config;
	}
}
MyJUnitLaunchShortcut

ということで出来上がったのが以下のソース。QuickJUnit0.5でQuickJUnitLaunchShortcutという名前が使われていたので、重複しないようにMyJUnitLaunchShortcutにしてみた。まぁ、中身についてはご容赦ください。とりあえず動けばいいの思想で作ったのでエレガントさのかけらもない感じですな。
起動構成を指定する場合、ILaunchConfigurationWorkingCopyインタフェースに対して、setAttributeメソッドを利用して指定する。基本的な設定に関してはIJavaLaunchConfigurationConstantsを利用して指定する。そのため、org.eclipse.jdt.launchingを追加した。あと、JUnit関連の設定はJUnitBaseLaunchConfigurationを利用する。また、テストランナーに関してはTestKindRegistryが保持する定数を利用する。
ちなみに、Projectのパスを取得しようとして最初はgetPathを使ったのだけど取れず、getLocationに行き着くのに30分ぐらいかかってしまった。。

package junit.extensions.eclipse.quick;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.jdt.internal.junit.launcher.JUnitBaseLaunchConfiguration;
import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchDescription;
import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchShortcut;
import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry;
import org.eclipse.jdt.internal.junit.ui.JUnitPlugin;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;

public class MyJUnitLaunchShortcut extends JUnitLaunchShortcut {
	public ILaunchConfiguration createConfiguration(JUnitLaunchDescription description) {
		ILaunchConfiguration config = super.createConfiguration(description);
		try {
			ILaunchConfigurationWorkingCopy wc= config.getWorkingCopy();
			Properties properties = new Properties();
			properties.load(new FileInputStream(description.getProject().getProject().getFile("QuickJUnit.properties").getLocation().toFile()));
			String testRunner = properties.getProperty("testRunner");
			if (testRunner != null && testRunner.equals("junit3")) {
				wc.setAttribute(
						JUnitBaseLaunchConfiguration.TEST_KIND_ATTR,
						TestKindRegistry.JUNIT3_TEST_KIND_ID);
			}
			String workingDirectory = properties.getProperty("workingDirectory");
			if (workingDirectory != null && workingDirectory.equals("") == false) {
				wc.setAttribute(
						IJavaLaunchConfigurationConstants.ATTR_WORKING_DIRECTORY, workingDirectory);
			}
			config = wc.doSave();
		} catch (FileNotFoundException e) {
			// 設定ファイルがなければデフォルトの挙動とする。
		} catch (IOException e) {
			JUnitPlugin.log(e);
		} catch (CoreException e) {
			JUnitPlugin.log(e);
		}
		return config;
	}
}

MyJUnitLaunchShortcutクラスを利用するようにするにはExtensionSupportのcreateJUnitLaunchShortcutメソッドを変更すればいい。本当はもっと正しいやり方がある気もするのだけど、まぁ動くからよしとする!

public class ExtensionSupport {
    public static ILaunchShortcut createJUnitLaunchShortcut() throws CoreException {
//        return createLaunchShortcut("org.eclipse.jdt.junit"); //$NON-NLS-1$
        return new MyJUnitLaunchShortcut(); //$NON-NLS-1$
    }
}

あとは、利用するプロジェクトの直下にQuickJUnit.propertiesというファイルを作成し、中身を以下のようにすれば動く。ほんとは.quickjunitみたいにするのだろうけど、その辺は余力があったら対応しよう。

testRunner = junit3
workingDirectory = C:/hoge

Eclipse3.5でのいじりかた

実は、3.2ではなく3.5から作ってたりする。で、3.5で作ったものを3.2で動かそうとしたところコンパイルエラーとなってしまった。。なので、3.5では3.2との差分のみ紹介する。ただし、重ねて言うが3.5ならばQuickJUnit0.5が使えるので、そちらを使った方がよいと思われる。
手順に関しては特に違いはない。org.eclipse.debug.coreがいらないぐらいかな。

  1. ソースの入手 http://github.com/kompiro/quick-junit/zipball/REL-0_4_0
  2. 解凍してEclipseにインポートする。ちなみに、Eclipse for RCP/Plug-in Developersパッケージ(eclipse-rcp-galileo-SR1-win32.zip)じゃないとプラグインのソースが含まれていない。3.2だと通常パッケージに入ってるんだけどね。
  3. Required Plug-insに以下を追加する。パッケージ構成が変わったため、org.eclipse.debug.coreはいらない。
  4. MyJUnitLaunchShortcutクラスを追加する。
  5. ExtensionSupportクラスのcreateJUnitLaunchShortcutメソッドでMyJUnitLaunchShortcutを利用するように変更する。
  6. 動作確認をするならばそのままEclipseアプリケーションとして起動する。
  7. 自分のEclipseに組み込むならば Deployable plug-ins and fragments としてexportする。
MyJUnitLaunchShortcut

JUnitLaunchShortcutクラスがinternalから公開パッケージ(org.eclipse.jdt.junit.launcher)に移動している。あと、JUnitの設定を行う定数がJUnitLaunchConfigurationConstantsに変わっているぐらい。JUnitLaunchShortcutクラスのソースはだいぶ変わっている。

package junit.extensions.eclipse.quick;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants;
import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry;
import org.eclipse.jdt.internal.junit.ui.JUnitPlugin;
import org.eclipse.jdt.junit.launcher.JUnitLaunchShortcut;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;

public class MyJUnitLaunchShortcut extends JUnitLaunchShortcut {
	protected ILaunchConfigurationWorkingCopy createLaunchConfiguration(
			IJavaElement element) throws CoreException {
		ILaunchConfigurationWorkingCopy wc = super
				.createLaunchConfiguration(element);
			try {
				Properties properties = new Properties();
				properties.load(new FileInputStream(element.getJavaProject().getProject().getFile("QuickJUnit.properties").getLocation().toFile()));
				String testRunner = properties.getProperty("testRunner");
				if (testRunner != null && testRunner.equals("junit3")) {
					wc.setAttribute(
							JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND,
							TestKindRegistry.JUNIT3_TEST_KIND_ID);
				}
				String workingDirectory = properties.getProperty("workingDirectory");
				if (workingDirectory != null && workingDirectory.equals("") == false) {
					wc.setAttribute(
							IJavaLaunchConfigurationConstants.ATTR_WORKING_DIRECTORY, workingDirectory);
				}
			} catch (FileNotFoundException e) {
				// 設定ファイルがなければデフォルトの挙動とする。
			} catch (IOException e) {
				JUnitPlugin.log(e);
			}

		return wc;
	}
}

まとめ

ようは起動構成関連のソースをちょこっといじっただけですな。ぜんぜん大したことはしておりません。でも、使いたい人とかいれば幸いです。もっとよくしてフィードバックしてください。
ちなみに、現場ではdjunitも利用しているので、こちらも同じことができるように対応を検討中です。やりたいことは起動構成をいじるだけなのでQuickJUnitと連携する必要が無いかも。あと、djunitはJUnitLaunchConfigurationもオーバーライドしているので、読み解くのは大変かも。それにソースは公開されているけどpluginプロジェクトの形式じゃないからその構築も面倒。いろいろやってみたけど、まだ動かないし。正月休みは別のことをする予定なので、djunit対応は来年かなぁ。

*1:そもそも違うものだった模様。しかも、0.5ではリリースから外されてた