Index: phpunit.xml
===================================================================
--- phpunit.xml	(revision 0)
+++ phpunit.xml	(working copy)
@@ -0,0 +1,14 @@
+<phpunit
+	bootstrap="tests/bootstrap.php"
+	backupGlobals="false"
+	colors="true"
+	convertErrorsToExceptions="true"
+	convertNoticesToExceptions="true"
+	convertWarningsToExceptions="true"
+	>
+	<testsuites>
+        <testsuite>
+            <directory prefix="bbp_" suffix=".php">tests/testcases/</directory>
+        </testsuite>
+    </testsuites>
+</phpunit>
\ No newline at end of file
Index: tests/bootstrap.php
===================================================================
--- tests/bootstrap.php	(revision 0)
+++ tests/bootstrap.php	(working copy)
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Bootstrap the plugin unit testing environment.
+ *
+ * Edit 'active_plugins' setting below to point to your main plugin file.
+ *
+ * @package wordpress-plugin-tests
+ */
+
+// Activates this plugin in WordPress so it can be tested.
+$GLOBALS['wp_tests_options'] = array(
+	'active_plugins' => array( 'bbpress/bbpress.php' ),
+);
+
+// If the develop repo location is defined (as WP_DEVELOP_DIR), use that
+// location. Otherwise, we'll just assume that this plugin is installed in a
+// WordPress develop SVN checkout.
+
+if( false !== getenv( 'WP_DEVELOP_DIR' ) ) {
+	require getenv( 'WP_DEVELOP_DIR' ) . '/tests/phpunit/includes/bootstrap.php';
+} else {
+	require '../../../../tests/phpunit/includes/bootstrap.php';
+}
+
+require( dirname( __FILE__ ) . '/includes/define-constants.php' );
+
+require BBP_TESTS_DIR . 'includes/testcase.php';
\ No newline at end of file
Index: tests/includes/define-constants.php
===================================================================
--- tests/includes/define-constants.php	(revision 0)
+++ tests/includes/define-constants.php	(working copy)
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * Define constants needed by test suite.
+ */
+
+define( 'BBP_PLUGIN_DIR', dirname( dirname( dirname( __FILE__ ) ) ) . '/' );
+
+if ( ! defined( 'BBP_TESTS_DIR' ) ) {
+	define( 'BBP_TESTS_DIR', dirname( dirname( __FILE__ ) ) . '/' );
+}
Index: tests/includes/factory.php
===================================================================
--- tests/includes/factory.php	(revision 0)
+++ tests/includes/factory.php	(working copy)
@@ -0,0 +1,86 @@
+<?php
+
+class BBP_UnitTest_Factory  extends WP_UnitTest_Factory {
+
+	/**
+	 * @var BBP_UnitTest_Factory_For_Forum
+	 */
+	public $forum;
+
+	/**
+	 * @var BBP_UnitTest_Factory_For_Topic
+	 */
+	public $topic;
+
+	/**
+	 * @var BBP_UnitTest_Factory_For_Reply
+	 */
+	public $reply;
+
+	function __construct() {
+		$this->forum = new BBP_UnitTest_Factory_For_Forum( $this );
+//		$this->topic = new BBP_UnitTest_Factory_For_Topic( $this );
+//		$this->reply = new BBP_UnitTest_Factory_For_Reply( $this );
+	}
+}
+
+class BBP_UnitTest_Factory_For_Forum extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'name'             => new WP_UnitTest_Generator_Sequence( 'Forum %s' ),
+			'description'      => new WP_UnitTest_Generator_Sequence( 'Forum description %s' ),
+			'slug'             => new WP_UnitTest_Generator_Sequence( 'forum-slug-%s' ),
+			'post_parent'      => $forum_parent_id,
+			'last_topic_id'    => 0,
+			'last_reply_id'    => 0,
+			'last_active_id'   => 0,
+			'last_active_time' => 0,
+		);
+	}
+
+	function create_object( $args ) {
+		if ( ! isset( $args['forum_author'] ) ) {
+			$args['forum_author'] = get_current_user_id();
+		}
+
+		$forum_id = bbp_new_forum( $args );
+
+		bbp_update_forum( $forum_id );
+
+		return $forum_id;
+	}
+
+	function update_object( $forum_id, $fields ) {
+		$forum = new BBP_Forums_Forum( $forum_id );
+
+		foreach ( $fields as $field_name => $value ) {
+			if ( isset( $forum->field_name ) )
+				$forum->field_name = $value;
+		}
+
+		$forum->save();
+		return $forum;
+	}
+
+	function get_object_by_id( $forum_id ) {
+		return new BBP_Forums_Forum( $forum_id );
+	}
+}
+/*
+class BBP_UnitTest_Factory_For_Topic extends WP_UnitTest_Factory_For_Post {
+
+	function create_object( $args ) {
+		return bbp_new_topic( $args );
+	}
+}
+
+class BBP_UnitTest_Factory_For_Reply extends WP_UnitTest_Factory_For_Thing {
+
+	function create_object( $args ) {
+		return bbp_new_reply( $args );
+	}
+}
+*/
Index: tests/includes/testcase.php
===================================================================
--- tests/includes/testcase.php	(revision 0)
+++ tests/includes/testcase.php	(working copy)
@@ -0,0 +1,255 @@
+<?php
+
+require_once dirname( __FILE__ ) . '/factory.php';
+
+class BBP_UnitTestCase extends WP_UnitTestCase {
+
+	protected $cached_SERVER_NAME = null;
+
+	public function setUp() {
+		parent::setUp();
+
+		// Make sure all users are deleted
+		// There's a bug in the multisite tests that causes the
+		// transaction rollback to fail for the first user created,
+		// which busts every other attempt to create users. This is a
+		// hack workaround
+		global $wpdb;
+		$wpdb->query( "TRUNCATE TABLE {$wpdb->users}" );
+
+		// Fake WP mail globals, to avoid errors
+		add_filter( 'wp_mail', array( $this, 'setUp_wp_mail' ) );
+		add_filter( 'wp_mail_from', array( $this, 'tearDown_wp_mail' ) );
+
+		$this->factory = new BBP_UnitTest_Factory;
+	}
+
+	function assertPreConditions() {
+		parent::assertPreConditions();
+
+		// Reinit some of the globals that might have been cleared by BBP_UnitTestCase::clean_up_global_scope().
+		// This is here because it didn't work in clean_up_global_scope(); I don't know why.
+		do_action( 'bbp_setup_globals' );
+	}
+
+	function go_to( $url ) {
+		// note: the WP and WP_Query classes like to silently fetch parameters
+		// from all over the place (globals, GET, etc), which makes it tricky
+		// to run them more than once without very carefully clearing everything
+		$_GET = $_POST = array();
+		foreach (array('query_string', 'id', 'postdata', 'authordata', 'day', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages', 'pagenow') as $v) {
+			if ( isset( $GLOBALS[$v] ) ) unset( $GLOBALS[$v] );
+		}
+		$parts = parse_url($url);
+		if (isset($parts['scheme'])) {
+			// set the HTTP_HOST
+			$GLOBALS['_SERVER']['HTTP_HOST'] = $parts['host'];
+
+			$req = $parts['path'];
+			if (isset($parts['query'])) {
+				$req .= '?' . $parts['query'];
+				// parse the url query vars into $_GET
+				parse_str($parts['query'], $_GET);
+			}
+		} else {
+			$req = $url;
+		}
+		if ( ! isset( $parts['query'] ) ) {
+			$parts['query'] = '';
+		}
+
+		// Scheme
+		if ( 0 === strpos( $req, '/wp-admin' ) && force_ssl_admin() ) {
+			$_SERVER['HTTPS'] = 'on';
+		} else {
+			unset( $_SERVER['HTTPS'] );
+		}
+
+		// Set this for bbp_core_set_uri_globals()
+		$GLOBALS['_SERVER']['REQUEST_URI'] = $req;
+		unset($_SERVER['PATH_INFO']);
+
+		// setup $current_site and $current_blog globals for multisite based on
+		// REQUEST_URI; mostly copied from /wp-includes/ms-settings.php
+		if ( is_multisite() ) {
+			$domain = addslashes( $_SERVER['HTTP_HOST'] );
+			if ( false !== strpos( $domain, ':' ) ) {
+				if ( substr( $domain, -3 ) == ':80' ) {
+					$domain = substr( $domain, 0, -3 );
+					$_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -3 );
+				} elseif ( substr( $domain, -4 ) == ':443' ) {
+					$domain = substr( $domain, 0, -4 );
+					$_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -4 );
+				}
+			}
+
+			$domain = rtrim( $domain, '.' );
+			$cookie_domain = $domain;
+			if ( substr( $cookie_domain, 0, 4 ) == 'www.' )
+				$cookie_domain = substr( $cookie_domain, 4 );
+
+			$path = preg_replace( '|([a-z0-9-]+.php.*)|', '', $GLOBALS['_SERVER']['REQUEST_URI'] );
+			$path = str_replace ( '/wp-admin/', '/', $path );
+			$path = preg_replace( '|(/[a-z0-9-]+?/).*|', '$1', $path );
+
+			$GLOBALS['current_site'] = wpmu_current_site();
+			if ( ! isset( $GLOBALS['current_site']->blog_id ) )
+				$GLOBALS['current_site']->blog_id = $wpdb->get_var( $wpdb->prepare( "SELECT blog_id FROM $wpdb->blogs WHERE domain = %s AND path = %s", $GLOBALS['current_site']->domain, $GLOBALS['current_site']->path ) );
+
+			// unit tests only support subdirectory install at the moment
+			// removed object cache references
+			if ( ! is_subdomain_install() ) {
+				$blogname = htmlspecialchars( substr( $GLOBALS['_SERVER']['REQUEST_URI'], strlen( $path ) ) );
+				if ( false !== strpos( $blogname, '/' ) )
+					$blogname = substr( $blogname, 0, strpos( $blogname, '/' ) );
+				if ( false !== strpos( $blogname, '?' ) )
+					$blogname = substr( $blogname, 0, strpos( $blogname, '?' ) );
+				$reserved_blognames = array( 'page', 'comments', 'blog', 'wp-admin', 'wp-includes', 'wp-content', 'files', 'feed' );
+				if ( $blogname != '' && ! in_array( $blogname, $reserved_blognames ) && ! is_file( $blogname ) )
+					$path .= $blogname . '/';
+
+				$GLOBALS['current_blog'] = get_blog_details( array( 'domain' => $domain, 'path' => $path ), false );
+
+				unset($reserved_blognames);
+			}
+
+			$GLOBALS['blog_id'] = $GLOBALS['current_blog']->blog_id;
+		}
+
+		unset($GLOBALS['wp_query'], $GLOBALS['wp_the_query']);
+		$GLOBALS['wp_the_query'] = new WP_Query();
+		$GLOBALS['wp_query'] = $GLOBALS['wp_the_query'];
+		$GLOBALS['wp'] = new WP();
+
+		// clean out globals to stop them polluting wp and wp_query
+		foreach ($GLOBALS['wp']->public_query_vars as $v) {
+			unset($GLOBALS[$v]);
+		}
+		foreach ($GLOBALS['wp']->private_query_vars as $v) {
+			unset($GLOBALS[$v]);
+		}
+
+		$GLOBALS['wp']->main($parts['query']);
+
+		// For bbPress, James.
+		$GLOBALS['bbp']->loggedin_user = NULL;
+		do_action( 'bbp_init' );
+	}
+
+	protected function checkRequirements() {
+		if ( WP_TESTS_FORCE_KNOWN_BUGS )
+			return;
+
+		parent::checkRequirements();
+
+		$tickets = PHPUnit_Util_Test::getTickets( get_class( $this ), $this->getName( false ) );
+		foreach ( $tickets as $ticket ) {
+			if ( 'BBP' == substr( $ticket, 0, 2 ) ) {
+				$ticket = substr( $ticket, 2 );
+				if ( $ticket && is_numeric( $ticket ) )
+					$this->knownBBPBug( $ticket );
+			}
+		}
+	}
+
+	/**
+	 * Skips the current test if there is an open BuddyPress ticket with id $ticket_id
+	 */
+	function knownBBPBug( $ticket_id ) {
+		if ( WP_TESTS_FORCE_KNOWN_BUGS || in_array( $ticket_id, self::$forced_tickets ) )
+			return;
+
+		if ( ! TracTickets::isTracTicketClosed( 'http://bbpress.trac.wordpress.org', $ticket_id ) )
+			$this->markTestSkipped( sprintf( 'bbPress Ticket #%d is not fixed', $ticket_id ) );
+	}
+
+	/**
+	 * WP's core tests use wp_set_current_user() to change the current
+	 * user during tests. BBP caches the current user differently, so we
+	 * have to do a bit more work to change it
+	 *
+	 * @global bbPres $bbp
+	 */
+	function set_current_user( $user_id ) {
+		global $bbp;
+		$bbp->loggedin_user->id             = $user_id;
+		$bbp->loggedin_user->fullname       = bbp_core_get_user_displayname( $user_id );
+		$bbp->loggedin_user->is_super_admin = $bbp->loggedin_user->is_site_admin = is_super_admin( $user_id );
+
+		wp_set_current_user( $user_id );
+	}
+
+	/**
+	 * When creating a new user, it's almost always necessary to have the
+	 * last_activity usermeta set right away, so that the user shows up in
+	 * directory queries. This is a shorthand wrapper for the user factory
+	 * create() method.
+	 *
+	 * Also set a display name
+	 */
+	function create_user( $args = array() ) {
+		$r = wp_parse_args( $args, array(
+			'role' => 'subscriber',
+			'last_activity' => bbp_core_current_time() - 60*60*24*365,
+		) );
+
+		$last_activity = $r['last_activity'];
+		unset( $r['last_activity'] );
+
+		$user_id = $this->factory->user->create( $args );
+
+		bbp_update_user_last_posted( $user_id, $last_activity );
+
+		return $user_id;
+	}
+
+	/**
+	 * We can't use grant_super_admin() because we will need to modify
+	 * the list more than once, and grant_super_admin() can only be run
+	 * once because of its global check
+	 */
+	public function grant_super_admin( $user_id ) {
+		global $super_admins;
+		if ( ! is_multisite() ) {
+			return;
+		}
+
+		$user = get_userdata( $user_id );
+		$super_admins[] = $user->user_login;
+	}
+
+	public function restore_admins() {
+		// We assume that the global can be wiped out
+		// @see grant_super_admin()
+		unset( $GLOBALS['super_admins'] );
+	}
+
+	/**
+	 * Set up globals necessary to avoid errors when using wp_mail()
+	 */
+	public function setUp_wp_mail( $args ) {
+		if ( isset( $_SERVER['SERVER_NAME'] ) ) {
+			$this->cached_SERVER_NAME = $_SERVER['SERVER_NAME'];
+		}
+
+		$_SERVER['SERVER_NAME'] = 'example.com';
+
+		// passthrough
+		return $args;
+	}
+
+	/**
+	 * Tear down globals set up in setUp_wp_mail()
+	 */
+	public function tearDown_wp_mail( $args ) {
+		if ( ! empty( $this->cached_SERVER_NAME ) ) {
+			$_SERVER['SERVER_NAME'] = $this->cached_SERVER_NAME;
+			unset( $this->cached_SERVER_NAME );
+		} else {
+			unset( $_SERVER['SERVER_NAME'] );
+		}
+
+		// passthrough
+		return $args;
+	}
+}
Index: tests/testcases/bbp_bbpress_tests.php
===================================================================
--- tests/testcases/bbp_bbpress_tests.php	(revision 0)
+++ tests/testcases/bbp_bbpress_tests.php	(working copy)
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * Tests to test that that testing framework is testing tests. Meta, huh?
+ *
+ * @package wordpress-plugins-tests
+ */
+class BBP_bbPress_Tests  extends WP_UnitTestCase  {
+
+	/**
+	 * Ensure that the plugin has been installed and activated.
+	 */
+	function test_plugin_activated() {
+		$this->assertTrue( is_plugin_active( 'bbpress/bbpress.php' ) );
+	}
+}