使用对象化实体模型进行模拟测试时的参考和关键问题



我不确定我是否应该干脆不这么做,但我通常习惯于嘲笑我的应用程序的很多领域,当我在实体内部使用Ref和Keys时会遇到问题。

我遵循了文档中的模式,你在实体中使用ref,但你屏蔽了ref,这样你就可以访问它引用的驱动程序对象,如下所示:

@Entity
class Car {
    @Id Long id;
    @Load Ref<Person> driver;    // Person is an @Entity
    public Person getDriver() { return driver.get(); }
    public void setDriver(Person value) { driver = Ref.create(value); }
}

我的特定场景是使用一个导入程序,在该导入程序中我解析xml并构建我的实体以准备保存它们。由于我的单元测试实际上只是测试xml的导入和解析,所以我模拟了我的dao实现,这样我就不会实际使用数据存储。

然后,当我调用setDriver(它只创建了一个Ref对象)时,我遇到了问题。然后,我使用getDriver方法,该方法将返回null,因为Ref.get直接依赖于数据存储。

有人遇到过这个问题吗?有没有办法创建一个mock Ref对象?我考虑在我的实体中不直接引用Ref,而是引用一个助手类,该类可以提供我在测试中可以控制的Ref?

选项的短列表为:

  1. 使用PowerMock
  2. 添加您自己的RefCreator对象
  3. 使用伪数据存储而不是嘲讽

我从不嘲笑数据存储层。本地单元测试线束是GAE最好的东西之一;它为您提供了一个功能完整的数据存储伪像:

https://cloud.google.com/appengine/docs/java/tools/localunittesting

我将Ref封装在Guava Supplier中,以避免在pojo的单元测试过程中依赖Objectify。供应商以与参考类似的方式转换为数据存储密钥

这个类主要是从Objectify RefTranslatorFactory复制的:

public class RefSupplierTranslatorFactory
    extends ValueTranslatorFactory<Supplier<?>, com.google.appengine.api.datastore.Key> {
@SuppressWarnings({ "unchecked", "rawtypes" })
public RefSupplierTranslatorFactory() {
    super((Class) Supplier.class);
}
@Override
protected ValueTranslator<Supplier<?>, com.google.appengine.api.datastore.Key> createValueTranslator(
        TypeKey<Supplier<?>> tk, CreateContext ctx, Path path) {
    final LoadConditions loadConditions = new LoadConditions(tk.getAnnotation(Load.class));
    return new ValueTranslator<Supplier<?>, com.google.appengine.api.datastore.Key>(
            com.google.appengine.api.datastore.Key.class) {
        @Override
        protected Supplier<?> loadValue(com.google.appengine.api.datastore.Key value, LoadContext ctx, Path path)
                throws SkipException {
            Ref<Object> ref = ctx.loadRef(Key.create(value), loadConditions);
            return new RefSupplier(ref);
        }
        @Override
        protected com.google.appengine.api.datastore.Key saveValue(Supplier<?> value, boolean index,
                SaveContext ctx, Path path) throws SkipException {
            return ctx.saveRef(Ref.create(value.get()), loadConditions);
        }
    };
}
public static class RefSupplier
        implements Serializable, Supplier<Object> {
    private static final long serialVersionUID = 1L;
    final private Ref<?> ref;
    public RefSupplier(Ref<?> ref) {
        this.ref = ref;
    }
    @Override
    public Object get() {
        return ref.get();
    }
}
}

假设我有以下Pojos:

@Entity
public static class CarWithSupplier {
    @Id
    Long id;
    Supplier<SteeringWheel> steeringWheel;
    List<Supplier<Tire>> tires;
}
@Entity
public static class SteeringWheel {
    @Id
    Long id;
}
@Entity
public static class Tire {
    @Id
    Long id;
}

我能够在不依赖Objectify的情况下运行单元测试:

@Test
public void testSupplier() {
    CarWithSupplier car = carWithSupplier();
    assertNotNull(car.steeringWheel);
    assertNotNull(car.tires);
    assertEquals(2, car.tires.size());
}
protected CarWithSupplier carWithSupplier() {
    CarWithSupplier car = new CarWithSupplier();
    car.steeringWheel = Suppliers.ofInstance(steeringWheel());
    final Supplier<Tire> leftFrontTire = Suppliers.ofInstance(tire());
    final Supplier<Tire> rightFrontTire = Suppliers.ofInstance(tire());
    car.tires = ImmutableList.of(leftFrontTire, rightFrontTire);
    return car;
}

扩展单元测试,但在测试设置期间设置必要的对象化资源,我能够在数据存储中运行相同的单元测试:

@Before
public void setUpObjectify() throws Exception {
    helper.setUp();
    closeable = ObjectifyService.begin();
    final ObjectifyFactory factory = ObjectifyService.factory();
    factory.getTranslators().add(new RefSupplierTranslatorFactory());
    factory.register(CarWithSupplier.class);
    factory.register(SteeringWheel.class);
    factory.register(Tire.class);
}
@Override
protected CarWithSupplier carWithSupplier() {
    final CarWithSupplier car = super.carWithSupplier();
    final Objectify ofy = ObjectifyService.ofy();
    Key<CarWithSupplier> key = ofy.save().entity(car).now();
    return ofy.load().key(key).now();
}
@Override
protected Tire tire() {
    final Tire tire = super.tire();
    ObjectifyService.ofy().save().entity(tire).now();
    return tire;
}
@Override
protected SteeringWheel steeringWheel() {
    final SteeringWheel steeringWheel = super.steeringWheel();
    ObjectifyService.ofy().save().entity(steeringWheel).now();
    return steeringWheel;
}

我的pojos的单元测试很有价值,因为它们最初是使用来自第三方web API服务的JSON响应(使用Gson)填充的。我发现将Gson解析的测试与对象化数据存储函数的测试分开是很有价值的。我后来在集成测试中对它们进行了全面测试。

我还没有广泛使用它,所以如果这可能会导致问题或以其他方式剥夺直接使用Ref的优势,我欢迎@stickpfigure提供意见。

我在这里编写了模拟Objectify的KeyRef类的代码。

使用:

Ref<MyEntity> ref = MockObjectify.ref(myEntity);

来源:

package present.objectify;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.apphosting.api.ApiProxy;
import com.google.common.cache.LoadingCache;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.Ref;
import com.googlecode.objectify.impl.KeyMetadata;
import com.googlecode.objectify.impl.Path;
import com.googlecode.objectify.impl.translate.CreateContext;
import java.util.Collections;
import java.util.Map;
import java.util.function.Supplier;
import present.engine.Caches;
/**
 * Creates Objectify mocks.
 *
 * @author Bob Lee
 */
public class MockObjectify {
  /** Creates a reference to the given instance. */
  public static <T> Ref<T> ref(T instance) {
    return new Ref<T>() {
      @Override public T get() {
        return instance;
      }
      @Override public boolean isLoaded() {
        return true;
      }
      @Override public Key<T> key() {
        return MockObjectify.key(instance);
      }
    };
  }
  /** Creates a key with a mock application ID. */
  public static <T> Key<T> key(T instance) {
    @SuppressWarnings("unchecked")
    KeyMetadata<T> metadata = (KeyMetadata<T>) keyMetadatas.getUnchecked(instance.getClass());
    return inMockEnvironment(() -> Key.create(metadata.getRawKey(instance)));
  }
  /** Creates a key with a mock application ID. */
  public static <T> Key<T> key(Class<? extends T> kindClass, long id) {
    KeyMetadata<T> metadata = keyMetadata(kindClass);
    return inMockEnvironment(() -> Key.create(KeyFactory.createKey(metadata.getKind(), id)));
  }
  /** Creates a key with a mock application ID. */
  public static <T> Key<T> key(Class<? extends T> kindClass, String name) {
    KeyMetadata<T> metadata = keyMetadata(kindClass);
    return inMockEnvironment(() -> Key.create(KeyFactory.createKey(metadata.getKind(), name)));
  }
  /** Creates a key with a mock application ID. */
  public static <T> Key<T> key(Key<?> parent, Class<? extends T> kindClass, long id) {
    KeyMetadata<T> metadata = keyMetadata(kindClass);
    return inMockEnvironment(() -> Key.create(KeyFactory.createKey(parent.getRaw(), metadata.getKind(), id)));
  }
  /** Creates a key with a mock application ID. */
  public static <T> Key<T> key(Key<?> parent, Class<? extends T> kindClass, String name) {
    KeyMetadata<T> metadata = keyMetadata(kindClass);
    return inMockEnvironment(() -> Key.create(KeyFactory.createKey(parent.getRaw(), metadata.getKind(), name)));
  }
  private static final ObjectifyFactory factory = new ObjectifyFactory();
  private static final LoadingCache<Class<?>, KeyMetadata<?>> keyMetadatas = Caches.create(
      type -> new KeyMetadata<>(type, new CreateContext(factory), Path.root()));
  @SuppressWarnings("unchecked")
  private static <T> KeyMetadata<T> keyMetadata(Class<? extends T> clazz) {
    return (KeyMetadata<T>) keyMetadatas.getUnchecked(clazz);
  }
  private static <T> T inMockEnvironment(Supplier<T> supplier) {
    ApiProxy.Environment original = ApiProxy.getCurrentEnvironment();
    try {
      ApiProxy.setEnvironmentForCurrentThread(mockEnvironment);
      return supplier.get();
    } finally {
      ApiProxy.setEnvironmentForCurrentThread(original);
    }
  }
  private static final ApiProxy.Environment mockEnvironment = new ApiProxy.Environment() {
    @Override public String getAppId() {
      return "mock";
    }
    @Override public String getModuleId() {
      throw new UnsupportedOperationException();
    }
    @Override public String getVersionId() {
      throw new UnsupportedOperationException();
    }
    @Override public String getEmail() {
      throw new UnsupportedOperationException();
    }
    @Override public boolean isLoggedIn() {
      throw new UnsupportedOperationException();
    }
    @Override public boolean isAdmin() {
      throw new UnsupportedOperationException();
    }
    @Override public String getAuthDomain() {
      throw new UnsupportedOperationException();
    }
    @Override public String getRequestNamespace() {
      throw new UnsupportedOperationException();
    }
    @Override public Map<String, Object> getAttributes() {
      return Collections.emptyMap();
    }
    @Override public long getRemainingMillis() {
      throw new UnsupportedOperationException();
    }
  };
}